Intel® oneAPI Threading Building Blocks
Ask questions and share information about adding parallelism to your applications when using this threading library.
2466 Discussions

Allocating an additional child of a non-running task with running children from multiple threads

michaelmarcin
Beginner
460 Views
I have an non running empty task used to synchronize some potentially long running work as suggested by the TBB book.

I don't know how many children tasks I need to create and I need to start running them before they are all created. The TBB book tells me I need to use allocate_additional_child_of(*empty) here.

It also tells me that the this pointer used for allocate_additional_child_of must be owned by the current thread of execution. However the only task I have to use as the this pointer is the empty task which is not running so I can't determine what thread owns it and even if I could I have no way to create my work if the thread that owns it is not the current thread.

Is there something I'm missing here?
0 Kudos
6 Replies
Alexey-Kukanov
Employee
460 Views
If you are inside the execute() method of another task, the this pointer is just right and you could simply omit it. Otherwise, use task::self().
0 Kudos
michaelmarcin
Beginner
460 Views
task::self seems like it might be what I need. What does it return if the thread is not currently running a task?

Below is a simplified example of my code with your suggestion integrated. The AddWork and WaitForWork functions should be safe to call concurrently from any thread that has initialized the tbb thread scheduler regardless of whether the thread is currently running a task or not. Is it valid?

// a real WorkTask would do something interesting
typedef tbb::empty_task WorkTask;

class WorkManager
{
tbb::task* CreateRoot()
{
return new( tbb::task::allocate_root() ) tbb::empty_task;
}

tbb::empty_task* m_rootTask;
tbb::spin_mutex m_mutex;

public:
WorkManager() : m_rootTask( CreateRoot() ) {}

void AddWork()
{
using namespace tbb;
spin_mutex::scoped_lock lock( m_mutex );
WorkTask& a = *new( task::self()->allocate_additional_child_of(*m_rootTask) ) WorkTask;
m_rootTask->spawn(a);
}

void WaitForWork()
{
using namespace tbb;
// create a new root so we can add more work while waiting for added work to complete
empty_task* root = CreateRoot();
{
spin_mutex::scoped_lock lock( m_mutex );
std::swap( m_rootTask, root );
}

root->wait_for_all();
// the task launcher is not actually run so we need to destroy it explicitly
root->destroy(*root);
}
};
0 Kudos
Alexey-Kukanov
Employee
460 Views

If the thread has initialized the TBB scheduler, task::self() will return a valid task object owned by this thread even if it does not run any task.

One thing to fix: as you call wait_for_all for your root tasks, each should have its reference counter set to 1 initially.

0 Kudos
michaelmarcin
Beginner
460 Views
OK then the reference return type makes a lot more sense thanks! I've incorporated your fix, thanks for catching that.

Are the TBB functions protected by the mutex in AddWork safe to call concurrently?
I was thinking of replacing spin_mutex with spin_rw_mutex and obtaining shared access in AddWork to allow it to run concurrently and exclusive access in the Wait function. Does this make sense as an optimization?

0 Kudos
Alexey-Kukanov
Employee
460 Views
The functions are safe to call concurrently. allocate_additional_child_of was specially designed for concurrent work with reference counter, and spawn works with the local task pool of the calling thread. Whether RW mutex will provide performance benefits, only benchmarking can say I think; a user class is constructed in the critical section, and you know whether it is heavy or not, so you are in better position to guess right than me :)
0 Kudos
michaelmarcin
Beginner
460 Views
Of course, Thanks.
0 Kudos
Reply