bshoshany / thread-pool

BS::thread_pool: a fast, lightweight, and easy-to-use C++17 thread pool library
MIT License
2.21k stars 253 forks source link

[REQ] Wait on any task in multi_future and thread_pool #146

Closed nolankramer closed 7 months ago

nolankramer commented 7 months ago

Describe the new feature

It is sometimes useful (especially for dependency graphs), to wait on any task in a group or pool to complete. This allows running downstream logic iteratively, while periodically checking/waiting for more task completions without polling.

A useful implementation may be adding two functions:

  1. thread_pool::wait_for_any(), which waits until any task in the pool completes.
  2. multi_future::wait_any(), which waits until any task in the the group completes.

thread_pool::wait_for_any()

The implementation for this seems simpler, since std::condition_variable is already used for queue management. A possible implementation might be:

/**
     * @brief Wait for any task to be completed. This does not wait on pending tasks if the queue is paused.
    */
    void wait_for_any()
    {
        std::unique_lock task_lock(tasks_mutex);
        waiting = true;
        tasks_done_cv.wait(task_lock, [this] { return (paused || tasks.empty()); });
        waiting = false;
    }

multi_future::wait_any()

This implementation seems much harder to get right, simply because the existing iterations of the C++ standard don't include a std::future::then(...) implementation.

A possible path forward may be to:

  1. Add a std::condition_variable and associated synchronization primitives to the multi_future members
  2. Make multi_future a friend of thread_pool
  3. Then add a function thread_pool::submitToMulti(...) that accepts a multi_future and submits the task to the pool, notifying the multi_future's std::condition_variable at the end.

(although this certainly lacks grace in that adding futures directly to the multi_future won't trigger the std::condition_variable...)

I've also seen some std::future::then(...) implementations floating around StackOverflow.

Perhaps even exposing a helper function like BS::create_future(...) that accepts a std::condition_variable would help mitigate some API headache.

Perhaps the answer is to create an entirely new helper class...

nolankramer commented 7 months ago

According to https://stackoverflow.com/questions/43614634/stdthread-how-to-wait-join-for-any-of-the-given-threads-to-complete, it may be best practice to use a producer-consumer queue that operates on the return value of the functions.

bshoshany commented 7 months ago

Hi @nolankramer and thanks for the suggestion! If I understand correctly, when you say "wait for any task" you mean "wait until exactly one task finishes running"? Would you be able to provide an example of a use case for this? You mentioned dependency graphs, how exactly will that work in that context?

Also, I'm not sure your suggested implementation will work as expected, because the predicate doesn't account for spurious wake-ups. The predicate will need to check that a task has actually finished running, which makes the implementation a bit more complicated. Maybe this could be done using a global counter that keeps track of many tasks have executed so far, with the wait function checking that the counter has been incremented, but that means complicating the entire thread pool. On the other hand, this could allow waiting for any specific number of tasks to complete, rather than just 1, which may also be useful.

Regarding the implementation for multi_future, I guess this could be implemented by blocking until ready_count() increases. multi_future is supposed to be independent of the thread pool, so the user can use it with their own futures independently if needed, so it can't be receiving any input from the thread pool itself.

If you tell me the exact use case you have in mind, I might be able to find a better solution. Thanks again!