Search code examples
c++tbbppl

PPL when_all with tasks of different types?


I'd like to use PPL "when_all" on tasks with different types. And add a "then" call to that task.

But when_all returns task that takes a vector, so all elements have to be the same type. So how do I do this?

This is what I have come up with but it feels like a bit of a hack:

//3 different types:
int out1;
float out2;
char out3;

//Unfortunately I cant use tasks with return values as the types would be  different ...
auto t1 = concurrency::create_task([&out1](){out1 = 1; }); //some expensive operation
auto t2 = concurrency::create_task([&out2](){out2 = 2; }); //some expensive operation
auto t3 = concurrency::create_task([&out3](){out3 = 3; }); //some expensive operation

auto t4 = (t1 && t2 && t3); //when_all doesnt block

auto t5 = t4.then([&out1, &out2, &out3](){
    std::string ret = "out1: " + std::to_string(out1) + ", out2: " + std::to_string(out2) + ", out3: " + std::to_string(out3);
    return ret;
});
auto str = t5.get();

std::cout << str << std::endl;

Anyone got a better idea?

(parallel_invoke blocks so I dont want to use that)


Solution

  • Task groups will work.

    Failing that:

    template<class...Ts>
    struct get_many{
      std::tuple<task<Ts>...> tasks;
      template<class T0, size_t...Is>
      std::tuple<T0,Ts...>
      operator()(
        std::task<T0> t0,
        std::index_sequence<Is...>
      ){
        return std::make_tuple(
          t0.get(),
          std::get<Is>(tasks).get()...
        );
      }
      template<class T0>
      std::tuple<T0,Ts...>
      operator()(task<T0> t0){
        return (*this)(
          std::move(t0),
          std::index_sequence_for<Ts...>{}
        );
      }
    };
    
    template<class T0, class...Ts>
    task<std::tuple<T0,Ts...>> when_every(
      task<T0> next, task<Ts>... later
    ){
      return next.then( get_many<Ts...>{
        std::make_tuple(std::move(later)...)
      } );
    }
    

    which doesn't work with void tasks, but otherwise bundles any set of tasks into a task of tuples.

    Getting voids to work is a bit more work. One way would be to write a get_safe that returns T for task<T> and void_placeholder for task<void>, then filter the resulting tuple before returning. Or write a partition_args that splits args into task<void> and task<T>, and act differently on the two of them. Both are a bit of a headache. We could also do a tuple-append pattern (where we deal with the tasks one-at-a-time, and can either append a T to the tuple or do nothing for void).

    It uses two C++14 library features (index_sequence and index_sequence_for) but both are easy to write in C++11 (2-4 lines each), and implementations are easy to find.

    I forget if task is copyable, I assumed it was not in the above code. If it is copyable, a shorter version of the above code will work. Apologies for any typos.