等待分离线程完成的正确方法是什么?

时间:2014-10-18 21:17:11

标签: multithreading c++11

请看这个示例代码:

void OutputElement(int e, int delay)
{
    this_thread::sleep_for(chrono::milliseconds(100 * delay));
    cout << e << '\n';
}

void SleepSort(int v[], uint n)
{
    for (uint i = 0 ; i < n ; ++i)
    {
        thread t(OutputElement, v[i], v[i]);
        t.detach();
    }
}

它启动n个新线程,每个线程在输出值并完成之前会休眠一段时间。在这种情况下,等待所有线程完成的正确/最佳/推荐方式是什么?我知道如何解决这个问题,但我想知道在这种情况下我应该使用的推荐多线程工具/设计(例如condition_variablemutex等...)?< / p>

2 个答案:

答案 0 :(得分:7)

现在对于略有反对的回答。我的意思是略微,因为我大多同意另一个答案和那些说“不要分离,而是加入”的评论。&#34;

首先想象没有join()。并且您必须使用mutexcondition_variable在您的主题中进行通信。这真的不是那么难,也不复杂。它允许任意丰富的通信,可以是任何你想要的,只要它只在互斥锁被锁定时进行通信。

现在,这种沟通的一个非常普遍的习惯用语只是说“我已经完成了”#34;。子线程将设置它,并且父线程将在condition_variable上等待,直到孩子说'#34;我完成了。&#34;这个成语实际上是如此常见,以至于需要一个封装mutexcondition_variable和州的便利函数。

join()正是这种便利功能。

但是我必须要小心。当一个人说:&#34;从不detach,始终join,&#34;这可以解释为:永远不要让你的线程沟通比#34;我已经完成了。&#34;

对于父线程和子线程之间更复杂的交互,请考虑父线程启动多个子线程以外出并独立搜索问题的解决方案的情况。当任何线程首次发现问题时,该问题将传达给父母,然后父母可以采用该解决方案,并告诉所有其他线程他们不再需要搜索。

例如:

#include <chrono>
#include <iostream>
#include <iterator>
#include <random>
#include <thread>
#include <vector>

void OneSearch(int id, std::shared_ptr<std::mutex> mut,
                   std::shared_ptr<std::condition_variable> cv,
                   int& state, int& solution)
{
    std::random_device seed;
//     std::mt19937_64 eng{seed()};
    std::mt19937_64 eng{static_cast<unsigned>(id)};
    std::uniform_int_distribution<> dist(0, 100000000);
    int test = 0;
    while (true)
    {
        for (int i = 0; i < 100000000; ++i)
        {
            ++test;
            if (dist(eng) == 999)
            {
                std::unique_lock<std::mutex> lk(*mut);
                if (state == -1)
                {
                    state = id;
                    solution = test;
                    cv->notify_one();
                }
                return;
            }
        }
        std::unique_lock<std::mutex> lk(*mut);
        if (state != -1)
            return;
    }
}

auto findSolution(int n)
{
    std::vector<std::thread> threads;
    auto mut = std::make_shared<std::mutex>();
    auto cv = std::make_shared<std::condition_variable>();
    int state = -1;
    int solution = -1;
    std::unique_lock<std::mutex> lk(*mut);
    for (uint i = 0 ; i < n ; ++i)
        threads.push_back(std::thread(OneSearch, i, mut, cv,
                          std::ref(state), std::ref(solution)));
    while (state == -1)
        cv->wait(lk);
    lk.unlock();
    for (auto& t : threads)
        t.join();
    return std::make_pair(state, solution);
}

int
main()
{
    auto p = findSolution(5);
    std::cout << '{' << p.first << ", " << p.second << "}\n";
}

上面我已经创造了一个假的问题&#34;其中一个线程搜索查询URNG所需的次数,直到它出现数字999.父线程放置5个子线程来处理它。子线程工作了一段时间,然后每隔一段时间,查找并查看是否还有其他线程找到了解决方案。如果是这样,他们会退出,否则他们会继续工作。主线程等待直到找到解决方案,然后加入与所有子线程。

对我来说,使用bash时间工具,输出:

$ time a.out
{3, 30235588}

real    0m4.884s
user    0m16.792s
sys 0m0.017s

但是,如果不是加入所有线程,它会分离那些尚未找到解决方案的线程。这可能看起来像:

    for (unsigned i = 0; i < n; ++i)
    {
        if (i == state)
            threads[i].join();
        else
            threads[i].detach();
    }

(代替上面的t.join()循环)。对我来说,这现在运行1.8秒,而不是4.9秒。即子线程通常不会相互检查,因此main只是分离工作线程并让操作系统将它们关闭。这对于此示例是安全的,因为子线程拥有它们所触摸的所有内容。没有什么东西可以从它们下面被破坏。

通过注意即使找到解决方案的线程也不需要加入,也可以实现最后一次迭代。 所有线程都可以分离。代码实际上要简单得多:

auto findSolution(int n)
{
    auto mut = std::make_shared<std::mutex>();
    auto cv = std::make_shared<std::condition_variable>();
    int state = -1;
    int solution = -1;
    std::unique_lock<std::mutex> lk(*mut);
    for (uint i = 0 ; i < n ; ++i)
        std::thread(OneSearch, i, mut, cv,
                          std::ref(state), std::ref(solution)).detach();
    while (state == -1)
        cv->wait(lk);
    return std::make_pair(state, solution);
}

表现仍然在1.8秒左右。

此处仍有(有点)与解决方案查找线程的有效连接。但它是使用condition_variable::wait而不是join完成的。

thread::join()是一个非常常见的习惯用法,你的父/子线程通信协议就是#34;我完成了。&#34;在这种常见情况下更喜欢thread::join(),因为它更容易阅读,更容易编写。

但是,不要不必要地将自己限制在如此简单的父/子通信协议中。当手头的任务需要时,不要害怕建立自己更丰富的协议。在这种情况下,thread::detach()通常会更有意义。 thread::detach()并不一定意味着一个发射后不停的线索。它可以简单地表示您的通信协议比我完成的更复杂。&#34;

答案 1 :(得分:2)

不要分离,而是加入

std::vector<std::thread> ts;

for (unsigned int i = 0; i != n; ++i)
    ts.emplace_back(OutputElement, v[i], v[i]);

for (auto & t : threads)
    t.join();