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.

Tasks are the safest when they have a copy of only the data they need for the task, and can then pass that data to subsequent tasks if necessary. Results can be passed via a carb::tasking::Future to the object waiting on the task. Alternately, keeping the data necessary to the task in a shared state (such as via std::shared_ptr / std::weak_ptr) is a good way to ensure that the lifetimes of the waiting object and the task are not intertwined: if the waiting object must be destroyed, it can release its reference while the task will see that the std::weak_ptr cannot be resolved and will exit without doing work.

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 contractual 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. For tasks, there is a similar concept in task-specific storage. See functions: ITasking::allocTaskStorage and ITasking::setTaskStorage for more information.

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.

Pinning to a Specific Thread

The carb.tasking plugin has means to ensure that a task resumes on the same thread as previously yielded it: PinGuard.

However, this can cause bottlenecks as specific threads required to run tasks may be busy with other tasks. Therefore, tasks may not resolve as quickly when pinning is used. To offset this, pinned tasks are the highest priority type of work for a thread to execute.

While pinning is generally not recommended, efficiency has been improved over previous versions of carb.tasking and it is a viable means to get around the mutex problem mentioned below.

Mutexes

All types of thread-based primitives (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. This is because these primitives are aware of the thread that owns them and the same thread must be used to release them. Carbonite primitives that are not within the carb::tasking namespace are also not fiber-aware.

Error

Holding a mutex lock when yielding a task will cause application errors!

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

See the debugging guide for identifying this problem.

Switch to a fiber-aware mutex

In many cases, std::mutex can be replaced with carb::tasking::MutexWrapper and std::recursive_mutex can be replaced with carb::tasking::RecursiveMutexWrapper. Similarly, std::shared_lock can be replaced with carb::tasking::SharedMutexWrapper. All of these classes correspond to the C++ named requirements of their counterparts, so they functions as “pin-compatible” drop-in replacements.

Reduce mutex lock scope

It should be evaluated whether a thread-based primitive should be held across a possible context switch. In some cases this is unavoidable, such as with nested locks. Locking thread-based primitives invoke mutual exclusion, meaning that of all of the threads that require the resource, only one may execute at a time. This contributes to bottlenecks.

Do not hold locks over callbacks

If a callback is called from a task, chances greatly increase that a context switch will occur from the callback function. It is important to avoid holding locks across callbacks, especially locks that are not fiber-safe. This is the third point of the Basic Callback Hygiene paradigm expressed in the coding standard.

The debugging guide presents debugging options that can help identify this anti-pattern.

Pinning as a last resort

To resolve potential issues in third-party libraries that cannot be modified, or libraries where it is not appropriate to integrate carb.tasking, pinning can be used in order to ensure that the task resumes on the same thread. However, this has significant performance implications.