Best Practices¶
Remember that carb.tasking.plugin is fiber-based–a cooperative multitasking situation. This means that hundreds or even thousands of tasks can be queued, running and yielding within the scheduler without the overhead of kernel scheduling. For these to cooperate the carb.tasking scheduler must be informed when a task needs to wait. This is accomplished by using the fiber-aware synchronization primitives which properly notify the scheduler and allow another task to run.
Think in Terms of Coroutines¶
A task within carb.tasking can be thought of as a Coroutine. Passing a Callable
object to carb::tasking::ITasking::addTask()
is akin to declaring a function async
. Similarly, getting the value from the returned carb::tasking::Future
or yielding on a carb::tasking::Counter
is akin to the await
keyword from Python.
It is also recommended to mark functions that run in a task as CARB_ASYNC
or CARB_MAYBE_ASYNC
. These
macros are intended to be human-readable only; they do not provide any additional information to the compiler. This is akin
to using the async
keyword from Python, without the contractural obligations. Within the body of a function that always
runs in a task, use one of the assertion macros: CARB_ASSERT_ASYNC
, CARB_CHECK_ASYNC
or :c:macro`CARB_FATAL_UNLESS_ASYNC`.
To aid in debugging, functions may also be declared CARB_NOINLINE
. This can help functions appear as distinct frames
in callstacks as opposed to obscurely-named lambdas.
No Thread-Specific Data¶
Since any thread may run a task, and a task may resume on a different thread than previously yielded the task, do not use Thread-Specific Data within task context.
Sleeping¶
Tasks may call ITasking::sleep_for
or ITasking::sleep_until
to
yield time to the carb.tasking scheduler. The scheduler will then select another task for the worker thread to run without
blocking the thread.
Suspend/Wake¶
Tasks may also suspend themselves with ITasking::suspendTask
and then
be woken by another task or thread with ITasking::wakeTask
. A common usage
paradigm for suspend/wake is I/O: a task that wants to block on I/O will issue the I/O request and then suspend itself with
suspendTask()
. When the I/O request is completed, the response can call wakeTask()
to resume the waiting task.
Determine if Running as a Task¶
All of the fiber-aware synchronization primitives also work properly when called from
a thread in that they determine whether the function is called from task context (i.e. as a fiber) or outside of a task (i.e. as a thread)
and either notify the scheduler to switch to a different task (task context) or block the current thread (thread context).
It may be desirable for a function to behave differently for a task versus a thread. To determine if code is being called
from task context, use carb::tasking::ITasking::getTaskContext
which will return kInvalidTaskContext
if not running in task context.
Mutexes¶
All types of mutex objects (including but not limited to std::mutex
, std::recursive_mutex
, std::shared_mutex
, pthread_mutex_t
, pthread_rwlock_t
, SRWLOCK
, CRITICAL_SECTION
)
are not fiber-aware and will cause issues if held when a task yields.
Error
Holding a mutex lock when yielding a task will cause application errors!
This is because these types of objects are aware of which thread holds the lock. Because tasks can yield and resume on different threads, this will cause errors. The carb.tasking plugin offers fiber-aware replacements for this purpose.
It is okay to use std::mutex
and std::recursive_mutex
for very short waits as long as there is no possibility of the
task yielding while holding the lock.
Note
For debugging yield-while-locked issues on Windows, there is a debug setting: debugLocks. This setting can affect performance, but it is recommended that this setting be set to true for non-performance-critical tests. The debugLocks setting is helpful for identifying the point where a yield occurs while holding a lock.
Use Pinning Only as a Last Resort¶
The carb.tasking plugin has means to ensure that a task resumes on the same thread as previously yielded it: PinGuard
.
Pinning a task to a thread is not efficient and can result in system bottlenecks, therefore it should only be done in
dire situations where there is no other easy recourse. Pinning a task will always result in a warning log message.