【问题标题】:How can completed futures be automatically erased from std::vector如何从 std::vector 中自动删除已完成的期货
【发布时间】:2017-07-16 00:55:14
【问题描述】:

在以下示例中,mEventExecutors 是 std::vector<std::future<void>>。我希望能够在完成时从向量中删除期货。这个可以吗?

void RaiseEvent(EventID messageID)
{
    mEventExecutors.push_back(std::move(std::async([=]{
            auto eventObject = mEventListeners.find(messageID);
            if (eventObject != mEventListeners.end())
            {
                for (auto listener : eventObject->second)
                {
                    listener();
                }
            }
        })
    ));
}

【问题讨论】:

  • 仅供参考:您实际上不需要在那里std::move,返回值已经是prvalue。
  • 你确定吗? cppreference 没有说明 std::async 返回右值。事实上,我会假设以下语句表明它确实需要移动。 如果从 std::async 获得的 std::future 没有从引用移动或绑定到引用,则 std::future 的析构函数将在完整表达式的末尾阻塞,直到异步操作完成,本质上使比如下面同步的代码
  • 是的。 std::move 实际上并没有移动任何东西(令人困惑,对吧?)。它只是将传递的值转换为 xvalue,从而为其启用移动语义。如果您查看签名,std::async 返回一个简单的std::future<...>。它不是一个引用,它是一个左值,但它是一个临时的,所以 std::move 是不必要的(为什么要将右值转换为右值?)。
  • 是的。不,RVO 和 NRVO 以及编译器使用的优化技术(标准允许)。您可能想阅读this question
  • 上面的代码似乎充满了同步错误。您以一种基本上不可能同步的方式访问mEventListeners。在 C++ 中处理线程的一种方法是“假设什么都没做——任务被推迟——直到你等待它”;你的程序还正确吗?您什么时候需要通知听众?如果没有,您可以编写代码,在线程退出或类似的情况下使用自动取消注册,并使用线程清理死线程和一些同步。

标签: c++ c++11 vector


【解决方案1】:

这个问题本身已经被另一个人回答了,但它激起了我的好奇心,想知道如何以最少的代码行数实现一个功能齐全、线程安全的任务管理器。

我还想知道是否可以将任务作为未来等待,或者可选地提供回调函数。

当然,这引出了一个问题,这些期货是否可以使用.then(xxx) 的性感延续语法而不是阻塞代码。

这是我的尝试。

非常感谢 boost::asio 的作者 Christopher Kohlhoff。通过研究他的出色工作,我了解到将类分为以下几类的价值:

  • handle - 控制对象的生命周期
  • 服务 - 提供对象逻辑、在对象 impl 之间共享的状态,并在实现对象的生命周期超过句柄(通常依赖回调的任何事情)时管理实现对象的生命周期,以及
  • 实现提供了每个对象的状态。

所以这里是调用代码的例子:

int main() {
    task_manager mgr;

    // an example of using async callbacks to indicate completion and error
    mgr.submit([] {
                   emit("task 1 is doing something");
                   std::this_thread::sleep_for(1s);
                   emit("task 1 done");
               },
               [](auto err) {
                   if (not err) {
                       emit("task 1 completed");
                   } else {
                       emit("task 1 failed");
                   }
               });

    // an example of returning a future (see later)
    auto f = mgr.submit([] {
        emit("task 2 doing something");
        std::this_thread::sleep_for(1500ms);
        emit("task 2 is going to throw");
        throw std::runtime_error("here is an error");
    }, use_future);

    // an example of returning a future and then immediately using its continuation.
    // note that the continuation happens on the task_manager's thread pool
    mgr.submit([]
               {
                   emit("task 3 doing something");
                   std::this_thread::sleep_for(500ms);
                   emit("task 3 is done");
               },
               use_future)
            .then([](auto f) {
                try {
                    f.get();
                }
                catch(std::exception const& e) {
                    emit("task 3 threw an exception: ", e.what());
                }
            });

    // block on the future of the second example
    try {
        f.get();
    }
    catch (std::exception &e) {
        emit("task 2 threw: ", e.what());
    }
}

这将导致以下输出:

task 1 is doing something
task 2 doing something
task 3 doing something
task 3 is done
task 1 done
task 1 completed
task 2 is going to throw
task 2 threw: here is an error

这是完整的代码(在比 gcc 更混杂的 apple clang 上测试过,所以如果我在 lambda 中遗漏了 this->,我深表歉意):

#define BOOST_THREAD_PROVIDES_FUTURE 1
#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION 1
#define BOOST_THREAD_PROVIDES_EXECUTORS 1

/* written by Richard Hodges 2017
 * You're free to use the code, but please give credit where it's due :)
 */
#include <boost/thread/future.hpp>
#include <boost/thread/executors/basic_thread_pool.hpp>
#include <thread>
#include <utility>
#include <unordered_map>
#include <stdexcept>
#include <condition_variable>

// I made a task an object because I thought I might want to store state in it.
// it turns out that this is not strictly necessary

struct task {

};

/*
 * This is the implementation data for one task_manager
 */
struct task_manager_impl {

    using mutex_type = std::mutex;
    using lock_type = std::unique_lock<mutex_type>;

    auto get_lock() -> lock_type {
        return lock_type(mutex_);
    }

    auto add_task(lock_type const &lock, std::unique_ptr<task> t) {
        auto id = t.get();
        task_map_.emplace(id, std::move(t));
    }

    auto remove_task(lock_type lock, task *task_id) {
        task_map_.erase(task_id);
        if (task_map_.empty()) {
            lock.unlock();
            no_more_tasks_.notify_all();
        }
    }

    auto wait(lock_type lock) {
        no_more_tasks_.wait(lock, [this]() { return task_map_.empty(); });
    }

    // for this example I have chosen to express errors as exceptions
    using error_type = std::exception_ptr;

    mutex_type mutex_;
    std::condition_variable no_more_tasks_;


    std::unordered_map<task *, std::unique_ptr<task>> task_map_;
};

/*
 * This stuff is the protocol to figure out whether to return a future
 * or just invoke a callback.
 * Total respect to Christopher Kohlhoff of asio fame for figuring this out
 * I merely step in his footsteps here, with some simplifications because of c++11
 */
struct use_future_t {
};
constexpr auto use_future = use_future_t();

template<class Handler>
struct make_async_handler {
    auto wrap(Handler handler) {
        return handler;
    }

    struct result_type {
        auto get() -> void {}
    };

    struct result_type result;
};

template<>
struct make_async_handler<const use_future_t &> {
    struct shared_state_type {
        boost::promise<void> promise;
    };

    make_async_handler() {
    }

    template<class Handler>
    auto wrap(Handler &&) {
        return [shared_state = this->shared_state](auto error) {
            // boost promises deal in terms of boost::exception_ptr so we need to marshal.
            // this is a small price to pay for the extra utility of boost::promise over
            // std::promise
            if (error) {
                try {
                    std::rethrow_exception(error);
                }
                catch (...) {
                    shared_state->promise.set_exception(boost::current_exception());
                }
            } else {
                shared_state->promise.set_value();
            }
        };
    }


    struct result_type {
        auto get() -> boost::future<void> { return shared_state->promise.get_future(); }

        std::shared_ptr<shared_state_type> shared_state;
    };

    std::shared_ptr<shared_state_type> shared_state = std::make_shared<shared_state_type>();
    result_type result{shared_state};

};

/*
 * Provides the logic of a task manager. Also notice that it maintains a boost::basic_thread_pool
 * The destructor of a basic_thread_pool will not complete until all tasks are complete. So our
 * program will not crash horribly at exit time.
 */
struct task_manager_service {

    /*
     * through this function, the service has full control over how it is created and destroyed.
     */

    static auto use() -> task_manager_service&
    {
        static task_manager_service me {};
        return me;
    }

    using impl_class = task_manager_impl;

    struct deleter {
        void operator()(impl_class *p) {
            service_->destroy(p);
        }

        task_manager_service *service_;
    };

    /*
     * defining impl_type in terms of a unique_ptr ensures that the handle will be
     * moveable but not copyable.
     * Had we used a shared_ptr, the handle would be copyable with shared semantics.
     * That can be useful too.
     */
    using impl_type = std::unique_ptr<impl_class, deleter>;

    auto construct() -> impl_type {
        return impl_type(new impl_class(),
                         deleter {this});
    }

    auto destroy(impl_class *impl) -> void {
        wait(*impl);
        delete impl;
    }

    template<class Job, class Handler>
    auto submit(impl_class &impl, Job &&job, Handler &&handler) {

        auto make_handler = make_async_handler<Handler>();


        auto async_handler = make_handler.wrap(std::forward<Handler>(handler));

        auto my_task = std::make_unique<task>();
        auto task_ptr = my_task.get();

        auto task_done = [
                this,
                task_id = task_ptr,
                &impl,
                async_handler
        ](auto error) {
            async_handler(error);
            this->remove_task(impl, task_id);
        };
        auto lock = impl.get_lock();
        impl.add_task(lock, std::move(my_task));
        launch(impl, task_ptr, std::forward<Job>(job), task_done);

        return make_handler.result.get();
    };

    template<class F, class Handler>
    auto launch(impl_class &, task *task_ptr, F &&f, Handler &&handler) -> void {
        this->thread_pool_.submit([f, handler] {
            auto error = std::exception_ptr();
            try {
                f();
            }
            catch (...) {
                error = std::current_exception();
            }
            handler(error);
        });
    }


    auto wait(impl_class &impl) -> void {
        impl.wait(impl.get_lock());
    }

    auto remove_task(impl_class &impl, task *task_id) -> void {
        impl.remove_task(impl.get_lock(), task_id);
    }


    boost::basic_thread_pool thread_pool_{std::thread::hardware_concurrency()};

};

/*
 * The task manage handle. Holds the task_manager implementation plus provides access to the
 * owning task_manager_service. In this case, the service is a global static object. In an io loop environment
 * for example, asio, the service would be owned by the io loop.
 */
struct task_manager {

    using service_type = task_manager_service;
    using impl_type = service_type::impl_type;
    using impl_class = decltype(*std::declval<impl_type>());

    task_manager()
            : service_(std::addressof(service_type::use()))
            , impl_(get_service().construct()) {}

    template<class Job, class Handler>
    auto submit(Job &&job, Handler &&handler) {
        return get_service().submit(get_impl(),
                                    std::forward<Job>(job),
                                    std::forward<Handler>(handler));
    }

    auto get_service() -> service_type & {
        return *service_;
    }

    auto get_impl() -> impl_class & {
        return *impl_;
    }

private:

    service_type* service_;
    impl_type impl_;
};


/*
 * helpful thread-safe emitter
 */
std::mutex thing_mutex;

template<class...Things>
void emit(Things &&...things) {
    auto lock = std::unique_lock<std::mutex>(thing_mutex);
    using expand = int[];
    void(expand{0,
                ((std::cout << things), 0)...
    });
    std::cout << std::endl;
}

using namespace std::literals;

int main() {
    task_manager mgr;

    // an example of using async callbacks to indicate completion and error
    mgr.submit([] {
                   emit("task 1 is doing something");
                   std::this_thread::sleep_for(1s);
                   emit("task 1 done");
               },
               [](auto err) {
                   if (not err) {
                       emit("task 1 completed");
                   } else {
                       emit("task 1 failed");
                   }
               });

    // an example of returning a future (see later)
    auto f = mgr.submit([] {
        emit("task 2 doing something");
        std::this_thread::sleep_for(1500ms);
        emit("task 2 is going to throw");
        throw std::runtime_error("here is an error");
    }, use_future);

    // an example of returning a future and then immediately using its continuation.
    // note that the continuation happens on the task_manager's thread pool
    mgr.submit([] {
                   emit("task 3 doing something");
                   std::this_thread::sleep_for(500ms);
                   emit("task 3 is done");
               },
               use_future)
            .then([](auto f) {
                try {
                    f.get();
                }
                catch (std::exception const &e) {
                    emit("task 3 threw an exception: ", e.what());
                }
            });

    // block on the future of the second example
    try {
        f.get();
    }
    catch (std::exception &e) {
        emit("task 2 threw: ", e.what());
    }
}

【讨论】:

  • 哇。这是很多代码。我还是要给予应有的重视。你确定handle/impl分离不是锤子,还有很多钉子吗?我从一开始就有点怀疑。我见过太多的模式只是“添加一层间接”的美化版本。当然,我会对此进行评估以找到它的本质。也许我会看到光明并在我这样做时回帖!
  • @sehe 当然,可以将所有这些逻辑合并到一个(或两个)类中,但是您将失去对接口和实现的关注点分离。这种方式允许我们编写一个(模板化的)句柄接口,该接口遵循一个(模板化的)服务,这允许我们实现各种类型的可复制性/异步性,除了可能需要一个策略类来专门化服务之外,无需更改任何代码。无论底层操作系统细节如何(例如 iOS、posix、emscripten 等),我都使用这种技术来提供相同的界面。
【解决方案2】:

在我看来,不要使用std::async 是最简单的解决方案,而是使用std::thread

但您需要小心,您的代码目前有很多数据竞争。考虑使用其他互斥锁或其他技术来防止它们。

std::thread{[=]() {
    // Task is running...
    auto eventObject = mEventListeners.find(messageID);
    if (eventObject != mEventListeners.end())
    {
        for (auto listener : eventObject->second)
        {
            listener();
        }
    }
}.detach(); // detach thread so that it continues

【讨论】:

  • 那么通过使用thread.detach,临时线程值超出范围也没关系?我在向量上推送期货的唯一原因是保持可运行的“范围内”,直到所有事件处理程序都为触发事件完成。
  • @Pat 没错,执行线程与std::thread 实例分离。
  • 太好了,我会测试一下看看效果如何。会尽快回复您
  • 我什至不需要计数,所以我没有使用代码的那个元素。我使用添加了 lock_guard 来同步对 mEventListeners 对象的访问。你能预见任何其他种族吗?
  • @Pat 每次访问mEventListeners 都需要锁定互斥锁(即使它在线程之外),listener() 也需要是线程安全的。
猜你喜欢
  • 2013-07-11
  • 2018-07-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-26
  • 2011-07-20
  • 2011-07-23
相关资源
最近更新 更多