Asynchronous Callable Wrappers

Contents[Show]

std::packaged_task enables you to write a simple wrapper for a callable, which you can invoke later.

std::packaged_task

To deal with std::packaged_task, you have to perform four steps usually:

  1. Wrap the task
  2. Create the future
  3. Perform the calculation
  4. Pick up the result

These steps are easy to get with an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// packagedTask.cpp

#include <utility>
#include <future>
#include <iostream>
#include <thread>
#include <deque>

class SumUp{
  public:
    int operator()(int beg, int end){
      long long int sum{0};
      for (int i= beg; i < end; ++i ) sum += i;
      return sum;
    }
};

int main(){

  std::cout << std::endl;

  SumUp sumUp1;
  SumUp sumUp2;
  SumUp sumUp3;
  SumUp sumUp4;

  // define the tasks
  std::packaged_task<int(int,int)> sumTask1(sumUp1);
  std::packaged_task<int(int,int)> sumTask2(sumUp2);
  std::packaged_task<int(int,int)> sumTask3(sumUp3);
  std::packaged_task<int(int,int)> sumTask4(sumUp4);

  // get the futures
  std::future<int> sumResult1= sumTask1.get_future();
  std::future<int> sumResult2= sumTask2.get_future();
  std::future<int> sumResult3= sumTask3.get_future();
  std::future<int> sumResult4= sumTask4.get_future();

  // push the tasks on the container
  std::deque< std::packaged_task<int(int,int)> > allTasks;
  allTasks.push_back(std::move(sumTask1));
  allTasks.push_back(std::move(sumTask2));
  allTasks.push_back(std::move(sumTask3));
  allTasks.push_back(std::move(sumTask4));
  
  int begin{1};
  int increment{2500};
  int end= begin + increment;

  // execute each task in a separate thread
  while ( not allTasks.empty() ){
    std::packaged_task<int(int,int)> myTask= std::move(allTasks.front());
    allTasks.pop_front();
    std::thread sumThread(std::move(myTask),begin,end);
    begin= end;
    end += increment;
    sumThread.detach();
  }

  // get the results
  auto sum= sumResult1.get() + sumResult2.get() + sumResult3.get() + sumResult4.get();

  std::cout << "sum of 0 .. 10000 = " << sum << std::endl;

  std::cout << std::endl;

}

 

The job of the program is not so heavy. Calculate the sum from 0 to 10000 with the help of four threads and sum up the results with the associated futures. Of course, you can use the Gaußschen Summenformel. (Strange, there is no English page, describing this famous algorithm. But math is international.).

In the first step, I pack the work packages in std::packaged_task (line 28 - 31) objects. Work packages are instances of the class SumUp (line 9 - 16). The current work is done in the call operator (line 11-15). The call operator sums up all numbers from beg to end and returns the sum as result. std::packaged_task in line 28 - 31 can deal with callables, that need two ints and return an int.

Now, I have to create in the second step the future objects with the help of std::packaged_task objects. Exactly that is done in the lines 34 to 37. The packaged_task is the promise in the communication channel. I define in these lines explicitly the type of the future: std::future<int> sumResult1= sumTask1.get_future(), but of course, I can do it also implicitly: auto sumResult1= sumTask1.get_future().

In the third step, the work takes place. The packaged_task are moved onto the std::deque (line 40 -44). In the while loop, each packaged_task (line 51-58) is executed. For doing that, I move the head of the std::deque in a std::packaged_task (line 52), move the packaged_task in a new thread (line 54) and let it run in the background (line 56). std::packaged_task object are not copyable. That's the reason, why I used move semantic in the lines 52 and 54. These restriction holds for all promises, but also futures and threads. There is one exception to this rule: std:.shared_future.

In the fourth and last step, the four futures ask with the get call for the results and sum them up (line 61).

Honestly, std::packaged_task was not made for simple use case like std::async. But the result is simple.

packagedTask

Optimization potential

C++11 has a function std::thread_hardware_concurrency. It provides a hint for the numbers of cores on your system. In case the C++ runtime has no glue, it's conforming to the standard to return 0. So you should verify that value in your program (line 31). With the current GCC, clang or Microsoft compiler, I get the right answer 4. I use this number of cores In the variation of the program packagedTask.cpp, to adjust the software to my hardware. So, my hardware is fully utilized.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// packagedTaskHardwareConcurrency.cpp

#include <algorithm>
#include <future>
#include <iostream>
#include <thread>
#include <deque>
#include <vector>

class SumUp{
public:
  SumUp(int b, int e): beg(b),end(e){}
  int operator()(){
    long long int sum{0};
    for (int i= beg; i < end; ++i ) sum += i;
    return sum;
  }
private:
    int beg;
    int end;
};

static const unsigned int hwGuess= 4;
static const unsigned int numbers= 10001;

int main(){

  std::cout << std::endl;

  unsigned int hw= std::thread::hardware_concurrency();
  unsigned int hwConcurr= (hw != 0)? hw : hwGuess;

  // define the functors
  std::vector<SumUp> sumUp;
  for ( unsigned int i= 0; i < hwConcurr; ++i){
    int begin= (i*numbers)/hwConcurr;
    int end= (i+1)*numbers/hwConcurr;
    sumUp.push_back(SumUp(begin ,end));
  }

  // define the tasks
  std::deque<std::packaged_task<int()>> sumTask;
  for ( unsigned int i= 0; i < hwConcurr; ++i){
    std::packaged_task<int()> SumTask(sumUp[i]);
    sumTask.push_back(std::move(SumTask));
  }

  // get the futures
  std::vector< std::future<int>> sumResult;
  for ( unsigned int i= 0; i < hwConcurr; ++i){
    sumResult.push_back(sumTask[i].get_future());
  }

  // execute each task in a separate thread
  while ( not sumTask.empty() ){
    std::packaged_task<int()> myTask= std::move(sumTask.front());
    sumTask.pop_front();
    std::thread sumThread(std::move(myTask));
    sumThread.detach();
  }

  // get the results
  int sum= 0;
  for ( unsigned int i= 0; i < hwConcurr; ++i){
    sum += sumResult[i].get();
  }

  std::cout << "sum of 0 .. 100000 = " << sum << std::endl;

  std::cout << std::endl;

}

 

What's next?

The next post will be about futures and promises. (Proofreader Alexey Elymanov)

 

 

 

 

 

 

 

 

title page smalltitle page small Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library".   Get your e-book. Support my blog.


 

 

 

Tags: tasks

Comments   

0 #21 Belen 2017-09-06 09:38
Great blog you have here.. It�s hardd to find good
quality writing like yours nowadays. I seriously appreciate indijviduals like you!
Take care!!
Quote

Add comment


My Newest E-Books

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 506

All 460335

Currently are 204 guests and no members online