我正在尝试编写一个简单的线程池,以了解它们如何在后台运行。不过,我遇到了一个问题。当我使用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);
}
感谢您的帮助!对不起,大代码发布。
答案 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
}
有一定的灵活性。但是有很多规则要遵循。
在设置消息数据和通知之间,必须锁定互斥锁。
您应该始终使用wait
的lambda版本-如果不使用lambda版本,则表示您做错了99次,共100次。
消息数据应该足以确定是否应该完成一项任务,如果不是讨厌的线程,锁和其他东西。
仅使用RAII方式锁定/解锁互斥锁。没有那的正确性几乎是不可能的。
在处理内容时不要握住锁。按住该锁足够长的时间以处理数据,然后放开该锁。
您的代码违反了2、3、4、5。我认为您没有搞错1。
但是,如果您在通知时保持对cv的锁定,现代cv实现实际上会非常高效。
我认为最明显的症状来自3:您的工作线程始终持有一个锁,因此只能进行锁定。其他会导致代码中出现其他问题。
现在,超越这种相对简单的模式成为可能。但是一旦完成,您实际上至少需要对C ++线程模型有基本的了解,并且无法可以通过编写代码并“看看它是否有效”来学习。您必须坐下来阅读规范,通读它们,了解条件变量在标准中的作用,了解互斥体的作用,编写一些代码,坐下来弄清为什么它不起作用,找其他写类似的东西的人代码,并且有问题,找出其他人如何调试它并发现错误,然后回到您的代码并找到相同的错误,进行调整,然后重复。
这就是为什么我使用条件变量编写基元,而不将条件变量与其他逻辑(例如,维护线程池)混合在一起的原因。
写一个线程安全的队列。它所做的只是维护队列,并在有要读取的数据时通知使用者。
最简单的一个有3个成员变量-互斥锁,条件变量和std队列。
然后使用关闭功能对其进行增强-现在pop必须返回一个可选参数或具有其他一些失败路径。
您的任务需要先将任务分批处理,然后再将其全部解雇。您确定要吗?为此,我要做的是将“推送多个任务”接口添加到线程安全队列中。然后在非线程安全队列中维护“未就绪”任务,并仅在我们希望线程使用它们时才将其全部推送。
然后,“线程池”消耗线程安全队列。因为我们分别编写了线程安全队列,所以我们有一半的活动部件,这意味着关系减少了4倍。
线程代码很难。尊重它。