Merge bitcoin/bitcoin#34576: threadpool: add ranged Submit overload

79571b9181 threadpool: add ranged Submit overload (Andrew Toth)

Pull request description:

  The current `ThreadPool::Submit` is not very efficient when we have a use case where we need to submit multiple tasks immediately. The `Submit` method must take the lock for each task, and notifies only a single worker thread. This will cause lock contention with the awakened worker thread trying to take the lock and the caller trying to submit the next task.

  Introduce a `Submit` overload, which takes the lock once and submits a range of tasks, then notifies all worker threads after the lock is released.

  This is needed for #31132 to be able to use `ThreadPool`.

ACKs for top commit:
  l0rinc:
    ACK 79571b9181
  rkrux:
    ACK 79571b9
  sedited:
    Re-ACK 79571b9181
  willcl-ark:
    ACK 79571b9181

Tree-SHA512: 1fbe0c150f01b9ea5be3459cd10b817045af52eaf6f14a1a298a68853890da4033c1b21bdc6f995bb55029fb4ab536e9dbf58d98e2e1e12b25298fa3470b4ba6
This commit is contained in:
merge-script
2026-03-11 13:08:53 +01:00
2 changed files with 107 additions and 3 deletions

View File

@@ -7,8 +7,8 @@
#include <sync.h>
#include <tinyformat.h>
#include <util/expected.h>
#include <util/check.h>
#include <util/expected.h>
#include <util/thread.h>
#include <algorithm>
@@ -16,6 +16,7 @@
#include <functional>
#include <future>
#include <queue>
#include <ranges>
#include <thread>
#include <type_traits>
#include <utility>
@@ -156,6 +157,15 @@ public:
Interrupted,
};
template <class F>
using Future = std::future<std::invoke_result_t<F>>;
template <class R>
using RangeFuture = Future<std::ranges::range_reference_t<R>>;
template <class F>
using PackagedTask = std::packaged_task<std::invoke_result_t<F>()>;
/**
* @brief Enqueues a new task for asynchronous execution.
*
@@ -171,9 +181,9 @@ public:
* uncaught exceptions, as they would otherwise be silently discarded.
*/
template <class F>
[[nodiscard]] util::Expected<std::future<std::invoke_result_t<F>>, SubmitError> Submit(F&& fn) noexcept EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
[[nodiscard]] util::Expected<Future<F>, SubmitError> Submit(F&& fn) noexcept EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
{
std::packaged_task<std::invoke_result_t<F>()> task{std::forward<F>(fn)};
PackagedTask<F> task{std::forward<F>(fn)};
auto future{task.get_future()};
{
LOCK(m_mutex);
@@ -186,6 +196,46 @@ public:
return {std::move(future)};
}
/**
* @brief Enqueues a range of tasks for asynchronous execution.
*
* @param fns Callables to execute asynchronously.
* @return On success, a vector of futures containing each element of fns's result in order.
* On failure, an error indicating why the range was rejected:
* - SubmitError::Inactive: Pool has no workers (never started or already stopped).
* - SubmitError::Interrupted: Pool task acceptance has been interrupted.
*
* This is more efficient when submitting many tasks at once, since
* the queue lock is only taken once internally and all worker threads are
* notified. For single tasks, Submit() is preferred since only one worker
* thread is notified.
*
* Thread-safe: Can be called from any thread, including within submitted callables.
*
* @warning Ignoring the returned futures requires guarding tasks against
* uncaught exceptions, as they would otherwise be silently discarded.
*/
template <std::ranges::sized_range R>
requires(!std::is_lvalue_reference_v<R>)
[[nodiscard]] util::Expected<std::vector<RangeFuture<R>>, SubmitError> Submit(R&& fns) noexcept EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
{
std::vector<RangeFuture<R>> futures;
futures.reserve(std::ranges::size(fns));
{
LOCK(m_mutex);
if (m_workers.empty()) return util::Unexpected{SubmitError::Inactive};
if (m_interrupt) return util::Unexpected{SubmitError::Interrupted};
for (auto&& fn : fns) {
PackagedTask<std::ranges::range_reference_t<R>> task{std::move(fn)};
futures.emplace_back(task.get_future());
m_work_queue.emplace(std::move(task));
}
}
m_cv.notify_all();
return {std::move(futures)};
}
/**
* @brief Execute a single queued task synchronously.
* Removes one task from the queue and executes it on the calling thread.