-
Notifications
You must be signed in to change notification settings - Fork 95
2.3 Blocking vs non blocking
As seen previously, coroutines are scheduled to run simultaneously on multiple coroutine threads, however they only run in "chunks" until a yield point is encountered following which the next coroutine "chunk" is executed. If any coroutine code were to make a typical blocking call, such as trying to lock a std::mutex or by calling std::this_thread::sleep_for() or even by logging to a file, the thread running all these coroutines will be suspended by the OS and as a result, preventing all other coroutines from running. Considering that a coroutine thread could potentially be servicing hundreds of thousands of coroutines simultaneously, such action would obviously result in a huge performance degradation, even if it only takes milliseconds. Therefore the golden rule of writing coroutine code is to avoid blocking at all cost.
For this purpose, specialized versions of mutexes, condition variables and spinlocks have been created which yield instead of block. Users must use these versions when protecting code areas which are shared between a regular IO thread (including main) and a coroutine thread, or between coroutines which run on separate threads.
On the other hand, in a traditional thread scenario such as used by the IO thread pool, suspending a thread only results in a single task being suspended.
All post()
, postFirst()
and postAsyncIo()
methods can be overloaded to specify a particular queue/thread to run on. By always grouping coroutines accessing the same shared object on a dedicated queue, it is possible to avoid locks altogether.
As seen previously, quantum can schedule user tasks in both coroutine thread or IO thread pools depending on the application. All dispatch API methods are accessible through public interfaces. Naming conventions specify Coro
or Thread
(e.g. ICoroFuture
vs IThreadFuture
) to indicate if the particular API should be used in a coroutine context or an IO thread context (including main). In most cases, the user will not have the option to choose and will only have access to the appropriate correct interface.
Methods which are to be used inside a coroutine and which can yield take an ICoroSync
interface as the first parameter. This will only be available inside a coroutine via it's context object which gets automatically injected, thus it is impossible to misuse. Methods exposed via Thread
interfaces do not have this parameter and will block instead of yield. Example:
//Called from a regular thread. This method blocks!
void ThreadContext<T>::wait();
//Called from a coroutine. This method yields!
void CoroContext<T>::wait(ICoroSyncPtr sync);