Preliminary. ThinkMeta ConcurrentTasks and this documentation are preliminary; everything is subject to change.

Lifetimes and sizing

ConcurrentTasks does not count references and does not grow containers. Every object has to be alive for as long as anyone can use it, and every bounded structure has to be large enough for the worst case. This page collects the rules; Performance first explains why.

Who must outlive whom

Object Must stay alive …
A task object from AddTask / Schedule until its OnTaskExited has been called
A LambdaTask from Task::Create the same — keep the returned object in a variable; and everything its callable captures by reference
An event, semaphore, critical section as long as any task can still wait on it, enter it or signal it, and until the last Set/Release has returned
A TaskGroup as long as tasks can still report to it — including the exit notification of the last task, which may still be running when Wait returns
An I/O request and its buffer until the operation has completed
A file or socket until no operation on it is pending
The scheduler until all its tasks have exited; Stop does not finish them
threadCallbackContext until the scheduler's threads have exited
Items on a ConcurrentStack or BasicStack while they are on the stack

Patterns that make it easy

Own primitives together with the tasks that use them. Put the tasks and the primitives they share into one object, start the tasks, and destroy the object only after all tasks have been joined. Nothing that a task touches can then disappear under it.

Join before you tear down. Wait for every task — with a TaskGroup, a WaitableTask or a final signal — before stopping the scheduler, disabling timers or asynchronous I/O, or destroying shared objects.

Prefer _s for one-shot handoffs. An _s event may be destroyed right after its waiter has returned, which makes "wait for this one result" patterns safe even when the event lives on the waiter's stack.

Keep objects in place. Tasks, primitives and requests must not move while they are in use; none of them can be copied, and most cannot be moved.

Sizing

What Rule
taskQueueCapacity larger than the most tasks that can be runnable at once per queue (new per priority, resumed per priority); suspended tasks do not count; default 4096
BoundedPointerQueue capacity at least the most items it can hold at once; enqueueing into a full queue breaks it — guard it with a semaphore that counts free slots
SpscVirtualMemoryPipe the producer writes at most the size it announced in Initialize
_s primitives at most one waiting task at a time

When in doubt, size for the worst case you can prove, not the case you measured.

See also