Temos um std::function
que precisa ser chamado após a conclusão de duas tarefas assíncronas diferentes. Queremos executar as duas tarefas ao mesmo tempo e não sabemos qual terminará primeiro.
Aqui está o código mais ou menos:
typedef std::function<void()> Callback;
void asyncCall(const Callback& callback)
{
std::async(std::launch::async, [callback](){
callback();
});
}
void startTaskOne(const Callback& callback)
{
asyncCall(callback);
}
void startTaskTwo(const Callback& callback)
{
asyncCall(callback);
}
void startTwoTasks(const Callback& callback)
{
/* ... */
}
int main()
{
std::cout << "started" << std::endl;
startTwoTasks([](){
std::cout << "finished" << std::endl;
});
return 0;
}
Quando fui confrontado pela primeira vez com este problema em C ++, lembrei-me que na verdade é muito semelhante ao resolvido pelas várias bibliotecas javascript assíncronas que podem ser usadas em node.js , onde tudo é assíncrono e pode facilmente acabar no temido inferno de retorno de chamada.
A implementação inicial se parece com isso.
void startTwoTasks(const Callback& callback)
{
auto counter = std::make_shared<std::atomic<int>>(2);
Callback parallel([counter, callback](){
--(*counter);
if(counter->load() == 0)
{
callback();
}
});
startTaskOne(parallel);
startTaskTwo(parallel);
}
A ideia é criarmos um ponteiro compartilhado para um contador com a quantidade total de etapas e, em seguida, quando cada etapa for concluída, diminuiremos o contador. Quando o contador chegar a zero, todas as etapas terão terminado e podemos chamar o callback. Além disso, neste exemplo, o contador é atômico, dessa forma as etapas podem ser executadas em threads diferentes.
Isso pode ser feito de uma maneira mais agradável, escrevendo uma classe de wrapper.
struct ParallelCallbackData
{
Callback callback;
std::atomic<int> counter;
std::atomic<bool> started;
ParallelCallbackData(const Callback& callback):
callback(callback), counter(0), started(false)
{
}
};
class ParallelCallback
{
private:
typedef ParallelCallbackData Data;
typedef std::shared_ptr<Data> DataPtr;
DataPtr _data;
static void step(const DataPtr& data)
{
if(data->counter-- == 0)
{
if(data->started && data->callback)
{
data->callback();
}
}
}
public:
ParallelCallback(const Callback& callback) :
_data(new Data(callback))
{
}
operator Callback()
{
assert(!_data->started);
++_data->counter;
return std::bind(&ParallelCallback::step, _data);
}
void check()
{
assert(!_data->started);
_data->started = true;
step(_data);
}
};
Agora o código parece muito mais limpo e é mais fácil adicionar tarefas adicionais.
void startTwoTasks(const Callback& callback)
{
ParallelCallback parallel(callback);
startTaskOne(parallel);
startTaskTwo(parallel);
parallel.check();
}
A vantagem de usar essa classe é que, ao sobrescrever o operador que converte de volta ao tipo de retorno de chamada original, podemos contar a quantidade de tarefas a serem realizadas. A desvantagem que isso apresenta é que podemos realmente acabar contando várias vezes para cima e para baixo e isso chamaria o retorno de chamada final várias vezes. Para resolver este problema, precisamos adicionar um booleano atômico adicional e um check
método. Além disso, ao invés de pré-incrementar o contador como na primeira implementação, nós o postamos, saindo de uma etapa adicional para a chamada de verificação no final.
Se quiséssemos fazer isso de maneira semelhante ao javascript, poderíamos adicionalmente envolver tudo em um modelo variável.
template<class Arg, class... Args>
void operator ()(Arg&& arg, Args&&... args)
{
arg(*this);
operator ()(args...);
}
template<class Arg>
void operator ()(Arg&& arg)
{
arg(*this);
check();
}
template<class Arg, class... Args>
static void run(Arg&& arg, Args&&... args)
{
ParallelCallback parallel(arg);
parallel(args...);
}
Poderíamos então reduzir o código a uma linha e ele pode ser usado com quantas tarefas forem necessárias.
void startTwoTasks(const Callback& callback)
{
ParallelCallback::run(callback, startTaskOne, startTaskTwo);
}
O código de trabalho para esses três exemplos pode ser encontrado aqui .