ChronoTrigger
ChronoTrigger

Reputation: 8617

Understanding concurrency of tbb::parallel_reduce

I am using the imperative form of tbb::parallel_reduce(range,body) as documented here. The doc says:

A parallel_reduce uses the splitting constructor to make one or more copies of the body for each thread. It may copy a body while the body’s operator()or method join runs concurrently. You are responsible for ensuring the safety of such concurrency.

So I understand that Body::Body( Body&, split ) can run as Body::operator() or Body::join( Body& rhs ) do.

My questions are:

1) Can Body::operator() and Body::join( Body& rhs ) be run concurrently?

2) Can Body::join( Body& rhs ) safely modify rhs?

W.r.t. 1), I believe join can be called after operator() is done only. Also, looking at the example in Intel's doc, it seems clear they can modify the same data without data race issues:

struct Sum {
    float value;
    ...
    void operator()( const blocked_range<float*>& r ) {
        value = ...;
    }
    void join( Sum& rhs ) {value += rhs.value;}
};               

Upvotes: 0

Views: 621

Answers (2)

Alexey Kukanov
Alexey Kukanov

Reputation: 12784

The lifetime of a Body object used by tbb::parallel_reduce is:

  1. It is created by the splitting constructor as the "right" side of another Body object, which in turn is updated to handle only the "left" side.
  2. Its operator() is applied to one or more consequent ranges.
  3. It can be used as the argument to splitting constructors of new bodies (yes, more than one time), and updated as per #1 above.
  4. Its method join() is called for each object split from it in #3.
  5. It is joined as rhs back to the body object it was split from.
  6. It is destroyed after joining.

Of the above operations, #3 can run concurrently to #2 and #4; all the rest is sequential, even though not necessarily called by a single thread. In other words, while a body accumulates partial reduction for some subrange(s) or joins the result from another body, it can be updated by splitting constructors of new bodies. This is where protection might be necessary, but typically these calls access different parts of the body.

So, the answers to your questions are:

1) Can Body::operator() and Body::join( Body& rhs ) be run concurrently?

No. join() is only called after all calls to operator(), both for this and rhs.

2) Can Body::join( Body& rhs ) safely modify rhs?

Yes, it can. But except possibly for moving its content during the join and updating the state accordingly, modifying rhs makes little sense because it will be destroyed right after the join() call.

Upvotes: 2

Anton
Anton

Reputation: 6537

Documentation is not clear enough on what the instances are involved in concurrent call to the methods. But it hints:

In typical usage, the safety requires no extra effort.

There is no concurrency for the same instance. So, it is safe to split or join both instances as in a serial code. operator() cannot be called concurrently on the same instance either. So that the documentation must really say:

It may copy a body while the body’s operator() or method join runs concurrently on different instances.

BTW, the quote

A parallel_reduce recursively splits the range into subranges to the point such that is_divisible() is false for each subrange.`

is true for simple_partitioner only, for other partitioners, it can stop splitting before reaching is_divisible() == false

Upvotes: 1

Related Questions