OS
v1.7.5
Documentation
|
A task coded as a Co-Routine, is just a task that allows multiple entry points for suspending and resuming execution at certain locations, this feature can bring benefits by improving the task cooperative scheme and providing a linear code execution for event-driven systems without complex state machines or full multithreading.
The QuarkTS++ implementation is stackless by using the Duff's device approach, and is heavily inspired by the Knuth method, Simon Tatham's Co-Routines in C and Adam Dunkels Protothreads . This means that a local-continuation variable is used to preserve the current state of execution at a particular place of the Co-Routine scope but without any call history or local variables. This brings benefits to lower RAM usage, but at the cost of some restrictions on how a Co-routine can be used.
Limitations and restrictions
static
.switch
statement.The application writer just needs to create the body of the Co-Routine . This means starting a Co-Routine segment with qOS::co::reenter() statement . From now on, yields and blocking calls from the Co-Routine scope are allowed.
The qOS::co::reenter() statement should be placed at the start of the task function in which the Co-routine runs.
A qOS::co::yield() statement return the CPU control back to the scheduler but saving the execution progress, thereby allowing other processing tasks to take place in the system. With the next task activation, the Co-Routine will resume the execution after the last qOS::co::yield() statement.
Blocking calls inside a Co-Routine should be made with the provided statements, all of them with a common feature: an implicit yield.
A widely used procedure is to wait for a fixed period of time. For this, the qOS::co::delay() should be used. This statement makes an apparent blocking over the application flow, but to be precise, a yield is performed until the requested time expires, this allows other tasks to be executed until the blocking call finish. This "yielding until condition meet" behavior its the common pattern among the other blocking statements.
Another common blocking call is qOS::co::waitUntil(). This statement takes a condition argument, a logical expression that will be performed when the Co-Routine resumes their execution. As mentioned before, this type of statement exposes the expected behavior, yielding until the condition is met.
An additional overload of this qOS::co::waitUntil() is also provided. This one sets a timeout for the logical condition to be met.
Optionally, the qOS::co::performUntil structure gives to application writer the ability to perform a multi-line job before the yield, allowing more complex actions to being performed after the Co-Routine resumes:
This feature provides positional local jumps, control flow that deviates from the usual Co-Routine call.
The complementary statements qOS::co::getPosition() and qOS::co::setPosition() provide this functionality. The first one saves the Co-Routine state at some point of their execution into CRPos
, a variable of type qOS::co::position , that can be used at some later point of program execution by qOS::co::setPosition() to restore the Co-Routine state to the one saved by qOS::co::getPosition() into CRPos. This process can be imagined to be a "jump" back to the point of program execution where qOS::co::getPosition() saved the Co-Routine environment.
This extension implements counting semaphores on top of Co-Routines. Semaphores are a synchronization primitive that provide two operations: wait and signal. The wait operation checks the semaphore counter and blocks the Co-Routine if the counter is zero. The signal operation increases the semaphore counter but does not block. If another Co-Routine has blocked waiting for the semaphore that is signaled, the blocked Co-Routines will become runnable again.
Semaphores are referenced by handles, a variable of type qOS::co::semaphore and must be initialized at time of creating with its own constructor. Here, a value for the counter is required. Internally, semaphores use an size_t
to represent the counter, therefore the value argument should be within range of this data-type.
To perform the wait operation, the qOS::co::semWait() statement should be used. The wait operation causes the Co-routine to block while the counter is zero. When the counter reaches a value larger than zero, the Co-Routine will continue.
Finally, qOS::co::semSignal() carries out the signal operation on the semaphore. This signaling increments the counter inside the semaphore, which eventually will cause waiting Coroutines to continue executing.
The following example shows how to implement the bounded buffer problem using Co-Routines and semaphores. The example uses two tasks: one that produces items and other that consumes items.
Note that there is no need for a mutex to guard the add_to_buffer()
and get_from_buffer()
functions because of the implicit locking semantics of Co-Routines, so it will never be preempted and will never block except in an explicit qOS::co::semWait() statement.
In certain situations, it may be necessary to control the flow of execution outside of the segment that defines the Co-routine itself. This is typically done to defer or resume the Co-routine in response to specific occurrences that arise in other contexts, such as tasks or interrupts.
To address these specific scenarios, a handler for the Co-routine must be defined, which is a variable of type qOS::co::handle. Additionally, to initiate the scope of the target Co-routine, the statement qOS::co::reenter() should be used taking this handler as argument.
As shown in the code snippet above, the Co-routine handle its globally declared to enable other contexts to access it. The example demonstrates how another task can control the Coroutine using the "try_" methods. It's important to note that the actions performed by this API can only be effective after the handle instantiation, which is a one-time operation that occurs during the first call of the Co-routine.