Important
This article can be a lot, but it is really important to understand for CPSC 213. I would recommend seeking your own resources as well for this topic as it is infinitely complex and interesting, and is tested.
A thread is an abstraction over asynchronous code created by Dijkstra that gives the illusion of a single, synchronous thread as opposed to the naive interrupt led version.
There are different thread systems, in C and in CPSC 213, we are using uthread.
A thread can have a variety of different states.
Every thread has its own stack, but share the rest of the memory space. This means that we need to have an implemented thread switch to switch contexts properly.
In CPSC 213, threads are controlled with a thread control block, or TCB.
There is also a list of TCBs for blocked threads that represent the blocked queue.
Thread States
A thread can be:
Nascent
The thread was just created/born. It is ready to run. Nascent threads are considered runnable, the only distinction is that they were just created.
Runnable
The thread is in a ready queue, and the TCB can choose to start running the thread.
Running
The thread is currently running. It is operating on its own procedures and stack, sharing the heap with other running threads.
Blocked
The thread is currently waiting. Usually this is with the help of a synchronization primitive like a mutex, in which case there can be multiple different queues of threads each waiting for a separate condition.
Dead
A thread is dead if it has completed its execution. It is marked for detachment and clean-up.
Freed
It is freed from memory.
Thread Operations
Creating a Thread
Creating a thread forks the control stream, splitting the execution of the program into another thread that executes in parallel of the current one. Similar in concept to how an async procedure call would split operations into two.
Created threads are marked as nascent. A nascent thread may not start right away. If there aren’t enough CPUs to run all thread, it will start when another thread yields, dies or blocks.
Blocking and Unblocking a Thread
You can block the current thread by saving the current thread’s state. It will stop so it can wait, and be but into the blocked queue. The TCB will then switch the core to a different thread.
A thread cannot be rescheduled until explicitly unblocked. Usually some criterion must be satisfied before the thread can be unblocked.
Unblocking a thread cannot be done by its own thread.
Joining a Thread
A join is a special kind of block where the condition for the resuming of the current thread is the target thread completes. In other words, the thread that calls join blocks itself and will only resume when the target thread has finished.
This is usually done to pick up what a thread has left when it finishes, for example a return value. This ‘unforks’ the execution of the program.
Joining threads is only relevant to the calling thread and the target thread, there still may be other threads running.
Detaching a Thread
Detaching a thread signals that no thread is going to be joined. If this thread is still running, it will be flagged so that as soon as it completes, it is freed. This is basically saying, hey man once you’re done just clean up after yourself and leave.
Yielding a Thread
Yielding a thread is saving a thread’s state and switching to a different runnable thread. Importantly, the thread that yields remains runnable. It does not put itself into a blocked state. The thread can therefore resume as soon as a core becomes available again, whether it’s because another thread yields or blocks, or because there were enough cores to fit all the threads in the first place.
Thread Implementation
Threads are implemented at a hardware and software level. The uthread implementation CPSC 213 uses is a software user-thread implementation that is conceptually similar to POSIX threads, though pthreads are kernel-level threads.
I would recommend going through the u_threads implementation to get a good idea of how it works.