Блокировка на многих шлюзах/фьючерсах и т.д. пока не будет готова

Можно ли блокировать группу блокировок/фьючерсов/любых блокируемых объектов, пока не будет готова какая-либо из них? Идея состоит в том, что мы можем сделать:

std::vector<std::future<T>> futures = ...;
auto ready_future = wait_until_an_element_is_ready(futures);
process(ready_future.get());

Я помню, что такие библиотеки, как libevent, libev и libuv, обладают такими способностями для задач ввода-вывода. Но я не знаю, можно ли это сделать для блокировок/фьючерсов.

Как я думал, чтобы добиться этого, нужно, чтобы фьючерсы вызывали обработчика после его завершения, но в то же время сравнивали и заменяли обработчик на нуль, чтобы другие фьючерсы не могли его назвать. Это требует координации фьючерсов, однако это не может быть сделано для блокировок.

ОБНОВЛЕНИЕ:. Похоже, что это предложение для него для С++ 2x.

Ответ 1

Если у вас есть возможность повышения, ее возможности многопоточности намного превышают стандартную библиотеку.

Это будет иметь место после С++ 17, а также при взгляде на вещи, даже после С++ 20.

#include <future>
#include <vector>

#define BOOST_THREAD_VERSION 4
#include <boost/thread.hpp>


void wait_for_any(std::vector<boost::future<void>> & v)
{
    boost::wait_for_any(std::begin(v), std::end(v));
}

Ответ 2

Альтернатива 1:

В настоящее время это невозможно, но вы можете обойти это, написав собственную логику поиска.

template<typename Iterator>
Iterator find_first_ready_future(Iterator begin, Iterator end)
{
    return std::find_if(begin, end, [](auto &future) { return future.wait_for(0s) == std::future_status::ready; }
}

template<typename T>
std::future<T> wait_until_an_element_is_ready(std::vector<std::future<T>> &vector)
{
    assert(!vector.empty());
    auto iterator = vector.end();
    do
    {
        // force switch of threads (if you don't want a busy loop, you might want to sleep this thread)
        // If reaction speed is very important, you might want to skip this first yield/sleep in the first iteration.
        std::this_thread::yield();

        iterator = find_first_ready_future(vector.begin(), vector.end());
    } while (iterator == vector.cend());
    auto result = std::move(*iterator);
    vector.erase(iterator); // Remove the ready future to prepare for the next call. (You ain't allowed to call .get() twice)
    return result;
}

Обратите внимание, что все фьючерсы должны быть созданы с помощью флага async, потому что это станет бесконечным циклом, если они "отложены".

PS: Если вы не хотите, чтобы это блокировалось для основного потока, вы можете выполнить его в своем потоке/будущем.

Альтернатива 2:

Другой альтернативой было бы обернуть ваши фьючерсы для выполнения задачи. Это немного похоже на предложение future.then:

template<typename T>
std::vector<std::future<void>> executeOnComplete(std::vector<std::future<T>> &&v)
{
    std::vector<std::future<void>> result;
    result.reserve(v.size());
    for (auto &f : v)
       result.emplace(std::async(std::launch::async,
                [f = std::move(f)] { process(f.get()); }));
    return result;
}

Эта альтернатива создает новый поток для каждого будущего и блокирует его до тех пор, пока исходное будущее не будет готово. Это связано с тем, что вы создаете слишком много потоков.

PS: С помощью какой-то фантастической логики шаблона result_of вы можете даже создавать фьючерсы, которые возвращают результат `process