Green threads, coroutines and fibers
Just as user threads are overlaid on kernel threads via the OS API, green threads are implemented entirely within the runtime or virtual machine provided by the programming language. In Python, scheduling responsibility for green threads is part of the interpreter process that runs the threads.
The following table summarizes the most important differences between Python’s native threads and green threads:
|
Aspect |
Threads |
Green threads |
|
Execution control |
Implemented via native operating system kernel, which means that a thread’s execution can be interrupted by the operating system at any time even when it is in the middle of an operation. |
Each thread runs until the scheduler interrupts its operation; scheduling mechanisms are implemented by the programming language. |
|
Portability |
Depends on the threading model implemented by the operating system, which means that race conditions and memory allocation depend on the OS rather than the program. |
Given that the implementation of the scheduler and thread model is native to the programming language, you can expect more consistent behavior across different runtime environments. |
|
Resource utilization |
Each thread has its own stack of resources, sharing memory allocated by the parent process. |
Runtime environment allocates isolated memory spaces per thread. |
|
Multi-processing |
Generally prevented by the global interpreter (CPython), but workarounds are possible. |
Not possible, as threads are bound by the master running process. |
Table 1.2: Characteristics of threads and green threads
Many programming languages have implemented green threads as their primary multitasking solution, but due to the limitations for multi-processing most have evolved to allow for cooperative multitasking through fibers and coroutines.
Fibers and coroutines
Fibers are like green threads in that they use a runtime scheduler that is independent of the underlying OS. However, instead of running until the scheduler interrupts their execution, fibers cooperate by ceding control to the next fiber in the same process. (Think of yarn being composed of multiple individual threads woven together.) This is also called cooperative multitasking.
A common drawback of fibers is that because scheduling control is passed to the developer, some fibers run or utilize resources over an extended period, reducing the resources available for execution of other fibers. Usually, fibers run inside a single thread.
The next step in the evolution of asynchronous processing is the coroutine, which is a function that can pause its own execution and later be resumed at the point at which it was interrupted. The following code starts the execution of a coroutine, then pauses its execution until some data is passed to resume the operation:
import datetime
def date_coroutine(_date:datetime.datetime):
print(f"Your appointment is scheduled for {_date.strftime('%m/%d/%Y, %H:%M:%S')}")
while True:
current_date = (yield)
if current_date > _date:
print("Oops, your appointment already passed")
else:
print("You have time")
d1 = datetime.datetime(1981, 6, 29, 1, 0)
coroutine = date_coroutine(d1)
coroutine.__next__()
d2 = datetime.datetime(2018, 5, 3)
coroutine.send(d2)
The date_coroutine is initialized with d1, but it’s not executed until the __next__() method is invoked. It starts by printing the value of the argument, then waits until data is passed via the send() method. Notice that there are two points of access to the method, and run time may vary. Coroutines will be explored deeply in Anchor 2.
Multitasking features are a two-way street – you need to communicate with them to understand whether they have been executed if you want to trigger another dependent process. Callbacks, futures, and promises are ways to manage these flows.