14 July 2023

C++ 核心指南目录

“Use async() to spawn concurrent tasks”

理由

与 R.12 类似。R.12 提出避免使用原始指针。这里我们提出要尽量避免原始线程和原始原语。应该用工厂函数,比如 std::async 来生成线程和重用线程,从而避免在你的代码中暴露原始线程。

例子

int read_value(const std::string& filename)
{
    std::ifstream in(filename);
    in.exceptions(std::ifstream::failbit);
    int value;
    in >> value;
    return value;
}

void async_example()
{
    try {
        std::future<int> f1 = std::async(read_value, "v1.txt");
        std::future<int> f2 = std::async(read_value, "v2.txt");
        std::cout << f1.get() + f2.get() << '\n';
    } catch (const std::ios_base::failure& fail) {
        // handle exception here
    }
}

注意

很遗憾, std::async 也不是很完美。比如说,它不能利用线程池。这意味着它不会把任务丢进队列等待执行,可能会因为资源耗尽而运行失败。但是,就算你不能利用 std::async 你也应该选择编写自己的函数,可以等待未来返回值。而尽量避免使用原始原语。

坏例子

这个例子显示了两种利用 std::future 的方式。但是无法避免的,需要管理原始 std::thread

void async_example()
{
    std::promise<int> p1;
    std::future<int> f1 = p1.get_future();
    std::thread t1([p1 = std::move(p1)]() mutable {
        p1.set_value(read_value("v1.txt"));
    });
    t1.detach(); // evil

    std::packaged_task<int()> pt2(read_value, "v2.txt");
    std::future<int> f2 = pt2.get_future();
    std::thread(std::move(pt2)).detach();

    std::cout << f1.get() + f2.get() << '\n';
}

好例子

这个例子展示了使用 std::async 的通用模式。如果程序环境不允许使用 std::async 本身的话,可以这么写:

void async_example(WorkQueue& wq)
{
    std::future<int> f1 = wq.enqueue([]() {
        return read_value("v1.txt");
    });
    std::future<int> f2 = wq.enqueue([]() {
        return read_value("v2.txt");
    });
    std::cout << f1.get() + f2.get() << '\n';
}

任何需要处理 read_value 的线程都隐藏在 WorkQueue::enqueue 之后。用户代码只要处理 future 对象,而不用处理原始线程、原始原语以及 packaged_task 对象。