Exception Handling and Cancellation in TBB - Part IV – Using context objects

After a good deal of theoretizing about various cancellation scenarios, we’ve finally reached the point where we can touch a bit more material substance (if one can say so about information☺). So let’s see how to use group contexts in practice.

You can create an instance of task group context using any technique applicable to C++ objects creation. That is there is no some tricky TBB function you have to use to create a context. There are three requirements which should be satisfied when you work with contexts. Two of them are for bound contexts only (remember that “bound” is one of two context flavors):

    • TBB scheduler have to be initialized before the moment when a bound context is constructed.

    • Destructor of a bound context have to be invoked in the same thread as its constructor.

    • When a context (of any flavor) is destroyed, there must be no tasks associated with it.



The following code snippet illustrates the recommended usage model that both satisfies the above requirements, and takes advantage of the fact that allocating objects on the local stack frame is the fastest way of creating non-static C++ objects.

 


void f () {
tbb::task_group_context C;
my_root_task &r = *new( tbb::task::allocate_root(C) ) my_root_task;
tbb::task::spawn_root_and_wait®;
}


By default a context is created as bound. But you may specify the context’s flavor explicitly:


tbb::task_group_context  context1(tbb::task_group_context::bound);
tbb::task_group_context context2(tbb::task_group_context::isolated);


There is only one way to explicitly associate a task with a context object (that is include the task into the specific cancellation group). You do it by using new overloaded version of tbb::task::allocate_root method as shown in the snippet above. This naturally means that only root tasks can be explicitly included into cancellation groups.

Non-root tasks are always included into the cancellation group of their parent.

If a root task is allocated without explicit context specification, it joins the cancellation group of the currently running task. If there is no currently running task (allocation takes place in the master thread) then the default context of this master thread is used.

A few more words about the default contexts. Each master thread has a default context of its own. Default contexts are created when tbb::task_scheduler_init object initializes the scheduler in the given thread, and are never reallocated during the scheduler lifetime. Default context is implicitly reset (cleaned up) each time when outermost algorithm using it finishes, which means that it is safe to reuse it even though you do not have direct access to it (you probably expected us to do it so, but it’s always good to be sure).

Only after we’ve released the first revision of TBB with cancellation support, we became aware that the name of the “bound” flavor can cause some misunderstanding regarding when the actual binding takes place. Since it’s probably too late to change the names, I’ll just make the necessary clarification here. So, when the constructor of a bound context C is executed, it registers it with the TBB scheduler but do not bind it with the parent context.

The association of a bound context C with its parent context is established only when method allocate_root(C) is called for the first time. When it is called from a task running in the context P, then P becomes parent of C. When allocate_root(C) is invoked directly from a master thread (not from a TBB task), then the default context is C’s parent.

So far we considered the low level operations that can be used to form cancellation groups. Since TBB provides higher level abstractions – parallel algorithms, it seems to be logical to allow specify contexts for algorithms too. Indeed at the moment prototypes of tbb::parallel_for and tbb::parallel_reduce algorithms are overloaded to accept the fourth argument of type tbb::task_group_context.


tbb::task_group_context ctx(tbb::task_group_context::isolated);
tbb::parallel_reduce (my_range, my_body,
tbb::auto_partitioner(), ctx);


Note that when you want to pass a context to one of the aforementioned algorithms, you’ll have to specify the partitioner explicitly (there is no overload accepting context and having default value for partitioner argument).

Older forms of parallel algorithms (without context argument) are of course preserved. Their behavior is to create new bound context for each algorithm instance. This means that if a nested algorithm is started without explicitly specified context, its cancellation will not affect its parent (outer algorithm).

If it is desirable that both outer algorithm and its children are cancelled as a whole, then context sharing can be used as in the next example:


class nested_body {
// ...
};

class outer_body {
tbb::task_group_context &my_ctx;
// . . .
public:
outer_body ( tbb::task_group_context& ctx ) : my_ctx(ctx) {}

void operator()( const my_range_t& r ) const {
my_nested_range_t nr = get_nested_range®;
tbb::parallel_for (nr, nested_body(),
tbb::simple_partitioner(), my_ctx);
return NULL;
}
};

void g () {
tbb::task_group_context ctx;
tbb::parallel_for (my_range_t(0,1000), outer_body(ctx),
tbb::auto_partitioner(), ctx);
}


At last one more example of using context objects for external cancellation of algorithms was given in the second part of my cancellation blog.

Just to avoid possible confusion, as I already mentioned above only tbb::parallel_for and tbb::parallel_reduce have been extended to the moment to accept contexts as their arguments, and only they internally use bound contexts when no context was explicitly specified. Other algorithms do not operate with contexts so far, and thus all the tasks created by them share the context of their caller.

Nevertheless I always talk about “parallel algorithms”, what obviously suggests “all TBB algorithms”, because this is a temporary situation, and we will extend the cancellation usage model of tbb::parallel_for and tbb::parallel_reduce to the rest of TBB algorithms in the near future. We have not done it right away because of time restriction imposed by our development cycle.

Well, it looks like that at this point I’ve finished with all the cancellation essentials, so the next time we’ll talk about exception handling in TBB, where you place try-blocks, how we propagate exceptions, and … that will be it! Keep patience!

For more complete information about compiler optimizations, see our Optimization Notice.