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

Cancelling/Stopping/Killing a Running Task

Karim_N_
New Contributor I
1,297 Views

Hi,

I'm a newbie when it comes to utilizing IntelTBB.

I am working on a Windows MFC application that is displaying interactive dashboards so user responsiveness is critical.
The data is complex not so much in terms of calculation (although some is involved) but in terms of the amount of
data that needs to be processed.  Each part of the dashboard can take several minutes to fully complete so information
is sent via boost::signals2 to the GUI level as it becomes available so that the screen can refresh and "appears" fast.

I've managed to update and maintain responsiveness during updates but I'm at a loss how to stop all running tasks
when the user switches to another dashboard view.  The tasks associated with the previous dashboard are still running,
taking up all resources.  The same problem manifests itself when I try and close the application.  It sucks when the
window disappears but the process is still running, taking up significant CPU resources.

I've tried the brute force method of tbb::task::destroy() but run into debug assertions that the victim task has
the incorrect state.  I know that's not elegant but I was grasping at straws.

An example task is below. I'm using std::function() because the original code was serial in nature and potentially
several classes will be involved in completing the task.

class MyTask : public tbb::task
{
public:
   MyTask(std::function<bool(int, int, int, int, int)> fnc, Message msg) : _fnc(fnc), _msg(msg) { }
   ~MyTask() {}

   tbb::task* execute()
   {
      _fnc(_msg.A, _msg.B, _msg.C, _msg.D, _msg.E);
      return nullptr;
   }

private:
   std::function<bool(int, int, int, int, int)> _fnc;
   Message _msg;
};

 

Another class has a dedicated worker thread to process messages in a queue.  When this "create" message comes along, the following method will be called on the worker thread:

 

void Manager::TASK_createMyTask(Message msg)
{
   tbb::task_scheduler_init init;
   using namespace std::placeholders;
   auto fn = std::bind(&Manager::processData, this, msg.A, msg.B, msg.D, msg.E, msg.F);
   MyTask& t = *new(tbb::task::allocate_root()) MyTask(fn, msg);
   tbb::task::spawn_root_and_wait(t);
}

 

Do I need to convert this to a "task group" in order to cancel it?  Can I manipulate a running task in any fashion so that it stops executing?

This previous post has been very helpful in setting up my initial foray into TBB: https://software.intel.com/en-us/forums/intel-threading-building-blocks/topic/302010

I've also been using a similar design as the GUI design pattern: https://software.intel.com/en-us/node/506119 ; The only difference is that I do not use a shared container with the GUI level nor do I use PostMessage since our backend is pure C++ and meant to be cross-platform.

Any help is greatly appreciated.  Thanks!

0 Kudos
7 Replies
Karim_N_
New Contributor I
1,297 Views

After some more research, I discovered this helpful blog by Andrey Marochko:

https://software.intel.com/en-us/blogs/2008/05/29/exception-handling-and-cancellation-in-tbb-part-ii-basic-use-cases

 

I've stared using a tbb::task_group_context as shown in the above article.

class createMyTask : public tbb::task
{
public:
   createMyTask(std::function<bool(int, int, int, int, int, tbb::task_group_context*)> fnc, Message msg) : _fnc(fnc), _msg(msg) { }
   ~createMyTask() {}

   tbb::task* execute()
   {
      _fnc(_msg.file, _msg.plot, _msg.origin, _msg.parameter, _msg.component, this->group());
      return nullptr;
   }

private:
   std::function<bool(int, int, int, int, int, tbb::task_group_context*)> _fnc;
   Message _msg;
};

The subsequent initialization now looks like this:

void Manager::TASK_createMyTask(Message msg)
{
   tbb::task_group_context* context = new tbb::task_group_context();
   using namespace std::placeholders;
   auto fn = std::bind(&Manager::processData, this, msg.A, msg.B, msg.C, msg.D, msg.E, context);
   MyTask* t = new(tbb::task::allocate_root(*context)) MyTask(fn, msg);
   _tasks.push_back(t);
   tbb::task::enqueue(*t);
}

I'm storing all tasks as a member so that when "Shutdown" is received by the Manager, it can iterate over each task and call cancel_group_execution.

It seems to work in halting running tasks but there is a downside in my current architecture.  The context needs to be added to the parameter list of many methods.  Each method then needs to check at various points as follows:

if (context->is_group_execution_cancelled())
{
   context->reset();
   return;
}

There is also a big warning in the task.h file about calling reset():

        "Because the method assumes that all the tasks that used to be associated with
         this context have already finished, calling it while the context is still
         in use somewhere in the task hierarchy leads to undefined behavior.

         IMPORTANT: This method is not thread safe!"

This gives me pause as to whether I am using the context correctly for cancellation, although the article does call reset in a similar fashion.

Any guidance or suggestions are appreciated.

0 Kudos
Alexei_K_Intel
Employee
1,297 Views

Let me clarify some moments. Your algorithm can have a group of tasks running when you want to add some another group of tasks. So you want to cancel the first group and allow the second group to utilize all available CPUs. Do you understand you correctly?

Regards,
Alex

0 Kudos
Karim_N_
New Contributor I
1,297 Views

Hi Alex,

Thank you for replying.

I think you are understanding correctly.  Let me clarify just to be clear:

  1. Window 1 data requires tasks 1, 2, and 3 to be spawned.
  2. CPU resources are consumed and data is incrementally passed back to the GUI level.
  3. Window 1 updates as data comes in but the tasks take a while and are still being processed.
  4. User does not want to wait and wants to see Window 2.
  5. Window 2 requires tasks 4, 5, and 6 to be spawned but before that happens, tasks 1, 2, and 3 need to be either killed or de-prioritized.

By the using the "group_context", I was able to kill the running tasks before spawning the new tasks for Window 2.  This is one solution but a better one would be to lower the priority of the currently running tasks and indicate the new tasks need to happen first.  I haven't yet looked into that but if you have any suggestions in that regard, it would be appreciated.

Thanks,

Karim

 

0 Kudos
Alexei_K_Intel
Employee
1,297 Views

If you have only two groups of tasks (related to two windows) you may want to consider task priority functionality. In addition, you can read the blog (however, it is a bit out dated, e.g. you do not need a preview macro).

You need to create two objects of the task_group_context type and adjust the priorities when required. The similar example can be found in examples/task_arena/fractal. See fractal.cpp:180-184.

Pay attention that priorities cannot be changed for already running tasks. Only tasks that are waiting for execution will be processed in accordance with priorities. I.e. you need to create a lot of tasks (e.g. with parallel_for) for the first window and the same for the second window.

Regards,
Alex

0 Kudos
Karim_N_
New Contributor I
1,297 Views

Hi,

I have since tried to incorporate priorities as suggested by the blog.  I've tried both dynamic and static priorities in the following way but haven't had success in terms of seeing an impact on execution.

void Manager::TASK_createMyTask(Message msg)
{
    tbb::task_group_context* context = new tbb::task_group_context();
    context->set_priority(msg.priority);
	   
	auto fn = std::bind(&Manager::processData, this, msg.A, msg.B, msg.C, msg.D, msg.E, context);
	MyTask* t = new(tbb::task::allocate_root(*context)) MyTask(fn, msg);
	_tasks.push_back(t);
	tbb::task::enqueue(*t);
}

void Manager::TASK_createMyTask(Message msg)
{
    tbb::task_group_context* context = new tbb::task_group_context();
        	   
	auto fn = std::bind(&Manager::processData, this, msg.A, msg.B, msg.C, msg.D, msg.E, context);
	MyTask* t = new(tbb::task::allocate_root(*context)) MyTask(fn, msg);
	_tasks.push_back(t);
	tbb::task::enqueue(*t, msg.priority);
}

In the first version, I set the priority in the group context.  In the second version, I set the priority when I enqueue the task.

In my test, I'm simply generating random data through a deliberately slow algorithm.  I queue up two tasks at virtually the same time, one low priority and one high priority.  They finish at roughly the same time.  If I queue up 15 low priority tasks and one high priority task, there doesn't seem to be much advantage given to the processing of the high priority task as again, several low priority tasks finish before or at the same time as the high priority one.

I was expecting virtually no work to be done on the low priority tasks until the high priority one was finished once it was scheduled.  Am I misunderstanding how this is expected to work?

Thanks.

0 Kudos
Alexei_K_Intel
Employee
1,297 Views

Hi Karim,

The priorities in Intel TBB runtime are used to decide what task should be executed next. It means that if there is no high priority tasks but CPU resources are available then the scheduler will assign the next available task (even with low priority) to the idle thread. Moreover, it is impossible to interrupt/stop the already executing tasks, i.e even if the dynamic priority is changed, the current tasks will continue their work.

It is possible for low priority tasks to finish before high priority tasks if they are shorter in execution time and there is no enough high priority tasks to utilize of available CPUs. Usually, it makes sense to create/spawn multiple tasks in algorithms that use dynamic priorities to change the scheduler execution order when required. If the algorithm spawns the only task, the priority change will be no-op if task started its execution.

A side questions:

  • In your samples the task group context is allocated dynamically and never deallocated. Looks like a memory leak. Or it is just a reduced example?
  • What is the "_tasks" container for? The tasks are deallocated by the scheduler automatically after execution, so the container is filled with potentially invalid pointers.

Regards,
Alex

0 Kudos
Karim_N_
New Contributor I
1,297 Views

Hi Alex,

Yes, I was not deallocating the context. Thanks for that.

The _tasks container is being used to cancel a running task on demand.  Each task corresponds to a window item so if the window is no longer in view because the user wants to see something else, the associated task is cancelled.  I cycle through the _tasks container to find the corresponding task ID and then issue a task->cancel_group_execution() command.  I suppose since these are running derived tasks is why I have not seen garbage data.

That is why I'm a bit confused by your statement "it is impossible to interrupt/stop the already executing tasks".  I thought this is what the group_context was for and it seems to be working successfully in my testing so far.

Since I can't change priorities of running tasks, I was thinking of deriving from task_group_context and adding a "is_group_execution_paused" method.  The code executed by the tbb::execute would evaluate this flag at runtime and busy-wait until it is asked to proceed.

Thanks,

Karim

0 Kudos
Reply