OS  v1.7.5
Documentation
Loading...
Searching...
No Matches
Co-Routines

Overview

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.

coroutines++
Coroutines in QuarkTS++

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

  • The stack of a Co-Routine is not maintained when a yield is performed. This means variables allocated on the stack will loose their values. To overcome this, a variable that must maintain its value across a blocking call must be declared as static.
  • Calls to API functions that could cause the Co-Routine to block, can only be made from the Co-Routine function itself - not from within a function called by the Co-Routine.
  • The implementation does not permit yielding or blocking calls to be made from within a switch statement.

Coding a Co-Routine

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.

void CoroutineTask_Callback( event_t e ) {
co::reenter() {
for (;;) {
if ( EventNotComing() ) {
co::yield();
}
DoTheEventProcessing();
co::delay( WAIT_TIME_MS );
PerformActions();
}
}
}

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.

Note
Co-Routine statements can only be invoked from the scope of the Co-Routine.
Remarks
You should put an endless-loop inside a Co-routine, this behavior is not hardcoded within the segment definition.

Blocking calls in a Co-routine

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:

co::perform() {
/*Job : a set of instructions */
} co::until( condition );
Warning
Co-routines statements are not allowed within the job segment.

Co-Routine usage example

void Sender_Task( event_t e ) {
co::reenter() {
Send_Packet();
/* Wait until an acknowledgment has been received, or until
* the timer expires. If the timer expires, we should send
* the packet again.
*/
co::waitUntil( PacketACK_Received(), TIMEOUT_TIME );
co::restart();
}
}
/*===================================================================*/
void Receiver_Task( event_t e ) {
co::reenter() {
/* Wait until a packet has been received*/
co::waitUntil( Packet_Received() );
Send_Acknowledgement();
co::restart();
};
}

Positional jumps

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.

Semaphores

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.

Co-Routine example with semaphores.

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.

#include "HAL.h"
#include "QuarkTS.h"
#include "AppLibrary.h"
using namespace qOS;
#define NUM_ITEMS ( 32 )
#define BUFSIZE ( 8 )
task_t ProducerTask, ConsumerTask;
co::semaphore mutex( 1 ), full( BUFSIZE ), empty( 0 );
/*===================================================================*/
void ProducerTask_Callback( event_t e ) {
static int produced;
co::reenter() {
for ( produced = 0 ; produced < NUM_ITEMS ; ++produced ) {
co::semWait( full );
co::semWait( mutex );
add_to_buffer( produce_item() );
co::semSignal( mutex );
co::semSignal( empty );
}
co::restart();
}
}
/*===================================================================*/
void ConsumerTask_Callback( event_t e ) {
static int consumed;
co::reenter() {
for ( consumed = 0 ; consumed < NUM_ITEMS ; ++consumed ) {
co::semWait( empty );
co::semWait( mutex );
consume_item( get_from_buffer() );
co::semSignal( mutex );
co::semSignal( full );
}
};
}
/*===================================================================*/
void IdleTask_Callback( event_t e ) {
/*nothing to do*/
}
/*===================================================================*/
int main(void) {
HAL_Init();
os.init( HAL_GetTick, IdleTask_Callback );
os.add( ProducerTask, ProducerTask_Callback, core::MEDIUM_PRIORITY,
100_ms, task::PERIODIC, taskState::ENABLED );
os.add( ConsumerTask, ConsumerTask_Callback, core::MEDIUM_PRIORITY,
100_ms, task::PERIODIC, taskState::ENABLED );
os.run();
return 0;
}
A Co-Routine Semaphore.
Definition coroutine.hpp:84
bool run(void) noexcept
Executes the scheduling scheme. It must be called once after the task pool has been defined.
bool add(task &Task, taskFcn_t callback, const priority_t p, const duration_t t, const iteration_t n, const taskState s=taskState::ENABLED_STATE, void *arg=nullptr) noexcept
Add a task to the scheduling scheme. The task is scheduled to run every t time units,...
bool init(const getTickFcn_t tFcn=nullptr, taskFcn_t callbackIdle=nullptr) noexcept
Task Scheduler initialization. This core method is required and must be called once in the applicatio...
The task argument with all the regarding information of the task execution.
Definition task.hpp:105
core & os
The predefined instance of the OS kernel interface.
OS/Kernel interfaces.
Definition bytebuffer.hpp:7

External control

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.

co::handle xHandleCR;
/*===================================================================*/
void AnotherTask_Callback( event_t e ) {
int UserInput = 0;
if ( e.firstIteration() ) {
xHandleCR.try_resume();
}
if ( e.lastIteration() ) {
xHandleCR.try_suspend();
}
UserInput = GetTerminalInput();
if ( UserInput == USR_RESTART ) {
xHandleCR.try_restart();
}
Perform_AnotherTask_Activities();
}
/*===================================================================*/
void CoroutineTask_Callback( event_t e ) {
co::reenter( xHandleCR ) { /*externally controlled*/
for(;;) {
if ( EventNotComing() ) {
co::yield();
}
RunFirstJob();
co::delay( WAIT_TIME );
SecondJobStatus = RunSecondJob();
co::waitUntil( JobFlag == JOB_SUCCESS, JOB_TIMEOUT );
CleanUpStatus = CleanupJob();
co::waitUntil( SomeVar > SomeValue );
}
};
}
A Co-Routine handle.
Definition coroutine.hpp:55
void try_restart(void) noexcept
Try to execute the co::restart() statement externally.
void try_resume(void) noexcept
Try to resume the coroutine execution externally after a suspend operation.
void try_suspend(void) noexcept
Try to suspend the coroutine execution externally.
bool firstIteration(void) const noexcept
Checks whether the current pass is the first iteration of the task. The value returned by this method...
Definition task.hpp:144
bool lastIteration(void) const noexcept
Checks whether the current pass is the last iteration of the task. The value returned by this method ...
Definition task.hpp:156

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.