std :: condition_variable :: notify_all()仅唤醒我的线程池中的一个线程

时间:2019-07-26 12:00:54

标签: c++ threadpool std condition-variable

我正在尝试编写一个简单的线程池,以了解它们如何在后台运行。不过,我遇到了一个问题。当我使用condition_variable并调用notify_all()时,它只会唤醒我池中的一个线程。

其他一切都很好。我已经排队900个工作,每个工作都有不错的工作量。唤醒的一个线程会消耗所有这些工作,然后返回睡眠状态。在下一个循环中,所有这些再次发生。

问题在于只有一个线程可以工作!我怎么弄这个模样呢?

ThreadPool.h:

#pragma once

#include <mutex>
#include <stack>
#include <atomic>
#include <thread>
#include <condition_variable>

class ThreadPool
{
friend void __stdcall ThreadFunc();

public:
    static ThreadPool & GetInstance()
    {
        static ThreadPool sInstance;

        return (sInstance);
    }

public:
    void AddJob(Job * job);
    void DoAllJobs();

private:
    Job * GetJob();

private:
    const static uint32_t ThreadCount = 8;

    std::mutex                  JobMutex;
    std::stack<Job *>           Jobs;
    volatile std::atomic<int>   JobWorkCounter;
    std::mutex                  SharedLock;
    std::thread                 Threads[ThreadCount];
    std::condition_variable     Signal;

private:
    ThreadPool();
    ~ThreadPool();

public:
    ThreadPool(ThreadPool const &) = delete;
    void operator = (ThreadPool const &) = delete;
};

ThreadPool.cpp:

#include "ThreadPool.h"

void __stdcall ThreadFunc()
{
    std::unique_lock<std::mutex> lock(ThreadPool::GetInstance().SharedLock);

    while (true)
    {
        ThreadPool::GetInstance().Signal.wait(lock);

        while (Job * job = ThreadPool::GetInstance().GetJob())
        {
            job->_jobFn(job->_args);
            ThreadPool::GetInstance().JobWorkCounter--;
        }
    }
}

ThreadPool::ThreadPool()
{
    JobWorkCounter = 0;

    for (uint32_t i = 0; i < ThreadCount; ++i)
        Threads[i] = std::thread(ThreadFunc);
}

ThreadPool::~ThreadPool()
{
}

void ThreadPool::AddJob(Job * job)
{
    JobWorkCounter++;

    JobMutex.lock();
    {
        Jobs.push(job);
    }
    JobMutex.unlock();
}

void ThreadPool::DoAllJobs()
{
    Signal.notify_all();

    while (JobWorkCounter > 0)
    {
        Sleep(0);
    }
}

Job * ThreadPool::GetJob()
{
    Job * return_value = nullptr;

    JobMutex.lock();
    {
        if (Jobs.empty() == false)
        {
            return_value = Jobs.top();
            Jobs.pop();
        }
    }
    JobMutex.unlock();

    return (return_value);
}

感谢您的帮助!对不起,大代码发布。

2 个答案:

答案 0 :(得分:0)

std::unique_lock<std::mutex> lock(ThreadPool::GetInstance().SharedLock);

每个线程首先获取此互斥体。

ThreadPool::GetInstance().Signal.wait(lock);

当主线程执行notify_all()时,所有线程都会从条件变量接收信号,但是您忘记了一个关键的细节:在被条件变量通知后醒来后,互斥锁会自动重新锁定。 wait()就是这样工作的。在您的C ++书籍或手册页中阅读其文档;并且只有一个线程将能够做到这一点。唤醒的所有其他线程也将尝试锁定互斥锁,但是只有第一个线程在比赛中获胜并且会这样做,其他所有线程将进入睡眠状态并继续做梦。

被通知后的线程将 wait()返回,直到该线程 也成功重新锁定了互斥锁

要从wait()返回,必须发生两件事:从条件变量 通知线程,该线程成功重新锁定了互斥锁。 wait()解锁互斥锁并等待条件变量 atomic ,并在收到通知后重新锁定互斥锁。

因此,幸运线程将锁定互斥锁,并耗尽所有作业的队列,然后返回循环顶部并再次wait()。这将解锁互斥锁,现在已经通知了其他一些幸运线程,但是耐心等待它晒晒阳光和荣耀的机会将能够锁定该互斥锁。以这种方式,所有其他线程将轮流使用,象大象一样,醒来,检查作业队列,在那儿什么也找不到,然后入睡。

这就是您看到此行为的原因。

要使显示的代码线程安全,必须完成两个基本操作。

1)您不需要两个互斥体,一个就足够了。

2)在对条件变量进行wait()之前,请检查作业队列中是否有内容。如果有东西,请将其删除, 并解锁互斥锁 ,然后执行该工作。

3)wait()仅在作业队列为空时。在wait()返回之后,重新锁定互斥锁,然后检查作业队列是否仍然为空(此时,我们不能真正保证它不为空,只是 可以 为非空)。

您只需要一个互斥锁即可保护对非线程安全作业队列的访问,并等待条件变量。

答案 1 :(得分:0)

除非您想设计一个新模式,否则使用条件变量的简单“ monkey-see monkey-do”方法总是要处理3件事情。

条件变量,互斥量和消息。

std::condition_variable cv;
mutable std::mutex m;
your_message_type message;

然后有3种模式可循。发送一条消息:

std::unique_lock l{m}; // C++17, don't need to pass type
set_message_data(message);
cv.notify_one();

发送大量消息:

std::unique_lock l{m};
set_lots_of_message_data(message);
cv.notify_all();

最后,等待并处理消息:

while(true) {
  auto data = [&]()->std::optional<data_to_process>{
    std::unique_lock l{m};
    cv.wait( l, [&]{ return done() || there_is_a_message(message); } );
    if (done()) return {};
    return get_data_to_process(message);
  }();
  if (!data) break;
  auto& data_to_process = *data;
  // process the data
}

有一定的灵活性。但是有很多规则要遵循。

  1. 在设置消息数据和通知之间,必须锁定互斥锁。

  2. 您应该始终使用wait的lambda版本-如果不使用lambda版本,则表示您做错了99次,共100次。

  3. 消息数据应该足以确定是否应该完成一项任务,如果不是讨厌的线程,锁和其他东西。

  4. 仅使用RAII方式锁定/解锁互斥锁。没有那的正确性几乎是不可能的。

  5. 在处理内容时不要握住锁。按住该锁足够长的时间以处理数据,然后放开该锁。

您的代码违反了2、3、4、5。我认为您没有搞错1。

但是,如果您在通知时保持对cv的锁定,现代cv实现实际上会非常高效。

我认为最明显的症状来自3:您的工作线程始终持有一个锁,因此只能进行锁定。其他会导致代码中出现其他问题。


现在,超越这种相对简单的模式成为可能。但是一旦完成,您实际上至少需要对C ++线程模型有基本的了解,并且无法可以通过编写代码并“看看它是否有效”来学习。您必须坐下来阅读规范,通读它们,了解条件变量在标准中的作用,了解互斥体的作用,编写一些代码,坐下来弄清为什么它不起作用,找其他写类似的东西的人代码,并且有问题,找出其他人如何调试它并发现错误,然后回到您的代码并找到相同的错误,进行调整,然后重复。

这就是为什么我使用条件变量编写基元,而不将条件变量与其他逻辑(例如,维护线程池)混合在一起的原因。

写一个线程安全的队列。它所做的只是维护队列,并在有要读取的数据时通知使用者。

最简单的一个有3个成员变量-互斥锁,条件变量和std队列。

然后使用关闭功能对其进行增强-现在pop必须返回一个可选参数或具有其他一些失败路径。

您的任务需要先将任务分批处理,然后再将其全部解雇。您确定要吗?为此,我要做的是将“推送多个任务”接口添加到线程安全队列中。然后在非线程安全队列中维护“未就绪”任务,并仅在我们希望线程使用它们时才将其全部推送。

然后,“线程池”消耗线程安全队列。因为我们分别编写了线程安全队列,所以我们有一半的活动部件,这意味着关系减少了4倍。

线程代码很难。尊重它。