实现condition_variable以解决多线程忙等待

时间:2017-02-02 10:43:12

标签: c++ multithreading c++11 condition-variable busy-waiting

我的程序通过使用空闲工作线程将多行文本打印到控制台。然而,问题是工人在打印文本之前没有等待先前的工作人员完成,这导致文本被插入到另一个工作线程的文本中,如下图所示:

enter image description here

我需要通过使用std :: condition_variable来解决这个问题 - 称为忙碌等待问题。我已经尝试在the example found at this link的基础上在下面的代码中实现condition_variable,而the following stackoverflow question帮助了我,但还不够,因为我对C ++的了解有限。所以最后我只是最后评论了一切,我现在不知所措。

// threadpool.cpp
// Compile with:
// g++ -std=c++11 -pthread threadpool.cpp -o threadpool

#include <thread>
#include <mutex>
#include <iostream>
#include <vector>
#include <deque>

class ThreadPool; // forward declare
//std::condition_variable cv;
//bool ready = false;
//bool processed = false;

class Worker {
public:
    Worker(ThreadPool &s) : pool(s) { }
    void operator()();
private:
    ThreadPool &pool;
};

class ThreadPool {
public:
    ThreadPool(size_t threads);
    template<class F> void enqueue(F f);
    ~ThreadPool();
private:
    friend class Worker;

    std::vector<std::thread> workers;
    std::deque<std::function<void()>> tasks;

    std::mutex queue_mutex;
    bool stop;
};

void Worker::operator()()
{
    std::function<void()> task;
    while (true)
    {
        std::unique_lock<std::mutex> locker(pool.queue_mutex);
        //cv.wait(locker, [] {return ready; });

        if (pool.stop) return;
        if (!pool.tasks.empty())
        {
            task = pool.tasks.front();
            pool.tasks.pop_front();
            locker.unlock();
            //cv.notify_one();
            //processed = true;
            task();
        }
        else {
            locker.unlock();
            //cv.notify_one();
        }
    }
}

ThreadPool::ThreadPool(size_t threads) : stop(false)
{
    for (size_t i = 0; i < threads; ++i)
        workers.push_back(std::thread(Worker(*this)));
}

ThreadPool::~ThreadPool()
{
    stop = true; // stop all threads

    for (auto &thread : workers)
        thread.join();
}

template<class F>
void ThreadPool::enqueue(F f)
{
    std::unique_lock<std::mutex> lock(queue_mutex);
    //cv.wait(lock, [] { return processed; });
    tasks.push_back(std::function<void()>(f));
    //ready = true;
}

int main()
{
    ThreadPool pool(4);

    for (int i = 0; i < 8; ++i) pool.enqueue([i]() { std::cout << "Text printed by worker " << i << std::endl; });

    std::cin.ignore();
    return 0;
}

2 个答案:

答案 0 :(得分:3)

这是一个工作样本:

// threadpool.cpp
// Compile with:
// g++ -std=c++11 -pthread threadpool.cpp -o threadpool

#include <thread>
#include <mutex>
#include <iostream>
#include <vector>
#include <deque>
#include <atomic>

class ThreadPool; 

// forward declare
std::condition_variable ready_cv;
std::condition_variable processed_cv;
std::atomic<bool> ready(false);
std::atomic<bool> processed(false);

class Worker {
public:
    Worker(ThreadPool &s) : pool(s) { }
    void operator()();
private:
    ThreadPool &pool;
};

class ThreadPool {
public:
    ThreadPool(size_t threads);
    template<class F> void enqueue(F f);
    ~ThreadPool();
private:
    friend class Worker;

    std::vector<std::thread> workers;
    std::deque<std::function<void()>> tasks;

    std::mutex queue_mutex;
    bool stop;
};

void Worker::operator()()
{
    std::function<void()> task;

    // in real life you need a variable here like while(!quitProgram) or your
    // program will never return. Similarly, in real life always use `wait_for`
    // instead of `wait` so that periodically you check to see if you should
    // exit the program
    while (true)
    {
        std::unique_lock<std::mutex> locker(pool.queue_mutex);
        ready_cv.wait(locker, [] {return ready.load(); });

        if (pool.stop) return;
        if (!pool.tasks.empty())
        {
            task = pool.tasks.front();
            pool.tasks.pop_front();
            locker.unlock();
            task();
            processed = true;
            processed_cv.notify_one();
        }
    }
}

ThreadPool::ThreadPool(size_t threads) : stop(false)
{
    for (size_t i = 0; i < threads; ++i)
        workers.push_back(std::thread(Worker(*this)));
}

ThreadPool::~ThreadPool()
{
    stop = true; // stop all threads

    for (auto &thread : workers)
        thread.join();
}

template<class F>
void ThreadPool::enqueue(F f)
{
    std::unique_lock<std::mutex> lock(queue_mutex);
    tasks.push_back(std::function<void()>(f));
    processed = false;
    ready = true;
    ready_cv.notify_one();
    processed_cv.wait(lock, [] { return processed.load(); });
}

int main()
{
    ThreadPool pool(4);

    for (int i = 0; i < 8; ++i) pool.enqueue([i]() { std::cout << "Text printed by worker " << i << std::endl; });

    std::cin.ignore();
    return 0;
}

输出:

Text printed by worker 0 
Text printed by worker 1 
Text printed by worker 2 
Text printed by worker 3 
Text printed by worker 4 
Text printed by worker 5 
Text printed by worker 6 
Text printed by worker 7

为什么不在生产代码中执行此操作

由于分配是按顺序打印字符串,因此该代码实际上可以真正实现并行化,因此我们设计了一种方法,使其完全依次使用std::condition_variable所需的Golden hammer。但至少我们摆脱了那个痴迷忙碌的等待!

在一个真实的例子中,您想要并行处理数据或执行任务,并同步只是输出,而这种结构仍然不是接近它的正确方法你是从零开始做的。

我改变了什么以及为什么

我使用原子bool作为条件,因为它们在多个线程之间共享时具有确定性行为。在所有情况下都不是绝对必要的,但不是一个好的做法。

while(true)循环中包含一个退出条件(例如由SIGINT处理程序或其他东西设置的标志),否则你的程序将从不退出。它只是一个任务,所以我们跳过它,但这一点非常重要,不要忽视生产代码。

也许这个任务可以通过一个条件变量来解决,但我对此并不确定,无论如何最好使用两个,因为它更清晰可读了每个人都。基本上,我们等待任务,然后让服务员等到它完成,然后告诉它它实际上已经处理完毕,我们已经为下一个工作做好了准备。你最初是在正确的轨道上,但我认为有两个cv,它更明显出错了。

此外,使用ready 之前设置条件变量(processednotify()非常重要。

我删除了locker.unlock(),因为案件是不必要的。 c ++ std锁是RAII结构,因此当锁超出范围时,锁将被解锁,这基本上就是下一行。一般来说,最好避免无意义的分支,因为你的程序会造成不必要的状态。

教育咆哮......

现在手头的问题已经得到解决和解决了,我认为有一些事情我认为需要对一般的任务进行说明,而且我认为这对你的学习来说可能比解决问题更重要如上所述。

如果您对作业感到困惑或沮丧,那么很好,你应该。你很难将一个方形钉子放入一个圆孔中,我认为这个问题的真正价值在于学会告诉你什么时候使用正确的工具来做正确的工作,当你没有和#39;吨

条件变量 是解决忙循环问题的正确工具,但是这个赋值(由@n.m。指出)是一个简单的竞争条件。也就是说,它只是一个简单的竞争条件,因为它包含一个不必要且执行不佳的线程池,使得问题复杂且难以理解,绝对没有目的。这就是说,std::async应该优先考虑现代c ++中的手工线程池(它既可以更容易正确实现,也可以在许多平台上更高效,并且不需要一堆全局和单身,并专门分配资源)。

如果这是你的老板而不是你的教授的任务,那么你就可以上交:

for(int i = 0; i < 8; ++i)
{
    std::cout << "Text printed by worker " << i << std::endl;
}

通过简单的for循环(最佳地)解决了这个问题。忙碌的等待/锁定问题是由于设计糟糕而导致的,而且#34;右边的&#34;要做的是修复设计,而不是绷带。我甚至不认为这项任务是有益的,因为没有可能的方式或理由来并行化输出,所以它最终会让每个人都感到困惑,包括SO社区。似乎是负面训练,线程只会在不改进计算的情况下引入不必要的复杂性。

实际上很难确定教授本人是否从作业结构中很好地理解了线程和条件变量的概念。必要时,为了培训目的,必须对任务进行简化,简化和略微淡化,但这实际上与此处所做的相反,其中复杂的问题是由一个简单的问题制成的。

作为一项规则,我从不在SO上回答与家庭作业相关的问题,因为我认为放弃答案会妨碍学习,而开发人员最有价值的技能就是学习如何在墙上砸到他们的头脑,直到一个想法突然出现。然而,除了像这样的人为的任务之外,只有负面的训练,虽然在学校你必须遵循教授的规则,但是当你看到它们时,学会识别人为的问题是很重要的。 ,解构它们,并找到简单而正确的解决方案。

答案 1 :(得分:1)

我认为这是正常的,因为互斥锁在打印前没有锁定。 对于循环中的每个转弯,我无法保证在i + 1之前打印。

为了获得良好的打印优先级,您应该在函数入队的互斥锁之后显示消息。