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.