任务队列有点像windows的消息循环,有任务被压进队列就执行任务,否者就一直等待任务的到来。这样做的好处是外部程序不需要关心队列线程中的同步,只需要将任务压进队列即可,非常的方便。比如现在有一个下载任务要做,没有线程队列的情况下要自己创建线程,编写线程回调,在线程中下载并写文件落盘,必要的时候还要通知界面下载进度,下载完成后还要将线程销毁,有一系列的细节要处理。有了任务队列后只需要编写下载任务,然后将其压到队列中去等待执行即可,这样就屏蔽了线程相关的细节,让重点落在任务的本身。当然也会有缺点,比如任务队列繁忙的时候就要等待其他任务执行完成;比如想要多个任务并行就要创建多个任务队列等。
任务是任务队列的执行单位(这不是废话么->->),一个任务应该有任务的开始入口点,这些入口点应该都一致,否者对于不同的任务任务队列就不好统一处理;任务还应该有一个停止入口,用来防止想要停止正在执行的任务。那么对于这个任务的简单抽象可以是这样的:
class ITask
{
public:
virtual ~ITask()
{
}
/** 执行任务
*/
virtual void DoTask() = 0;
/** 停止任务
*/
virtual void Stop() = 0;
};
使用者只要继承该接口,实现他的DoTask() 和Stop() 即可,如果需要参数的可以放到成员变量中去。
有的时候我们希望在任务执行的过程中可以给外部一些反馈,比如下载任务通知外部下载的进度等,我们可以这样来定义任务:
class DownTask :
public ITask
{
public:
DownTask(const std::function& callback) :
m_callback(callback)
{
}
virtual ~DownTask()
{
}
/** 执行任务
*/
virtual void DoTask()
{
// 下载的代码,可以调用CallBack来通知外界进度
}
/** 停止任务
*/
virtual void Stop()
{
// 如果需要的可以实现这个接口,一般是定义一个标记量flag,DoTask每次循环判断
// 标记,如果为假就停止
}
void CallBack(int all, int cur)
{
if (m_callback != NULL)
{
m_callback(all, cur);
}
}
protected:
/** 回调
*/
std::function m_callback;
};
需要注意的是,回调函数是在线程中执行的,访问全局资源是有同步问题的,所以一般来说最好不要在回调中访问需要同步的全局资源,或者直接在回调中封装一个任务压到另外一个队列中去,比如下载进程中可以在回调中发消息到主界面(主界面的消息循环实际上也相当与一个任务队列),对于下载进程来说可以将窗口的句柄直接给任务对象,就不需要callback了,需要说明的是这样做并不是很好,因为我们并不知道这个句柄是否还有效,它有可能已经失效了变成一个野指针(给一个无效但是不为空的句柄发消息是一个未定义行为,可以使用week_ptr来确保指针的有效,但是本文并不讨论)。
创建一个类,里面维护一个线程和一个队列,在线程的回调函数中不停的循环检测队列中是否有有效的任务,如果有就执行,没有就等待。比如:
class SimpleTaskQueue :
public NonCopyable
{
public:
/** 构造函数
*/
SimpleTaskQueue();
/** 析构函数
*/
~SimpleTaskQueue();
public:
/** 开始循环
@return 是否成功
*/
bool Start()
{
m_pThread = new (std::nothrow) std::thread(&SimpleTaskQueue::ThreadCallBack, this);
return (m_pThread != NULL);
}
public:
/** 线程回调
*/
virtual void ThreadCallBack()
{
while (1)
{
if (m_taskQue.size() > 0)
{
auto task = m_taskQue.front();
m_taskQue.pop();
task->DoTask();
}
}
}
private:
/** 线程
*/
std::thread* m_pThread;
/** 任务队列
*/
std::queue > m_taskQue;
};
有了循环后还需要将任务压进队列里的接口,在定义接口之前还要考虑一个问题,线程中一直不停的取队列,另外一个线程中向队列里压任务,那么就需要同步,将循环改成:
/** 线程回调
*/
virtual void ThreadCallBack()
{
while (1)
{
std::shared_ptr spTask = NULL;
// 取出队首
{
std::lock_guard lock(m_mutex);
if (m_taskQue.empty())
{
continue;
}
spTask = m_taskQue.front();
m_taskQue.pop();
if (spTask == nullptr)
{
continue;
}
}
spTask->DoTask();
}
}
需要注意的是DoTask一定不能放到花括号内被mutex保护。压任务的代码:
void PushTask(const std::shared_ptr& task)
{
std::lock_guard lock(m_mutex);
m_taskQue.push(task);
}
好了简单任务队列就完成了。
上述代码其实并没有什么用,有两个问题:
1、没有停止循环的接口,也没有停止当前任务的接口;这个问题很好解决,想要停止循环只要加上一个flag即可,想要停止当前任务只要将当前执行的任务记录下来,需要停止的时候调用任务的stop即可。
2、没有任务的时候循环一直空转,占用了大量的cup。这个问题有两个解决方案:
a:循环时没有任务时候加上Sleep(0)释放cpu的使用权。(这个方法简单却不是很好,但是可以衍生出另外一种方法,会在后续文章中给出,本文不讨论)
b:使用事件event,没有任务的时候等待任务,有任务的时候执行,很像生产者消费者模型,只不过只有一个消费者而已。
对于第一个问题,修改代码如下:
class SimpleTaskQueue :
public NonCopyable
{
public:
/** 构造函数
*/
SimpleTaskQueue():m_stop(false),m_pThread(nullptr){};
/** 析构函数
*/
~SimpleTaskQueue()
{
if (m_pThread != nullptr)
{
StopAll();
m_pThread->join();
delete m_pThread;
m_pThread = nullptr;
}
};
public:
/** 开始循环
@return 是否成功
*/
bool Start()
{
m_pThread = new (std::nothrow) std::thread(&SimpleTaskQueue::ThreadCallBack, this);
return (m_pThread != NULL);
}
void StopAll()
{
std::lock_guard lock(m_mutex);
ClearAll();
m_stop = true;
}
void StopCurrent()
{
std::lock_guard lock(m_mutex);
if (m_currentTask != nullptr)
{
m_currentTask->Stop();
}
}
void ClearAll()
{
std::lock_guard lock(m_mutex);
StopCurrent();
ClearQueue();
}
void ClearQueue()
{
std::lock_guard lock(m_mutex);
std::queue > tmp = std::queue >();
m_taskQue.swap(tmp);
}
void PushTask(const std::shared_ptr& task)
{
std::lock_guard lock(m_mutex);
m_taskQue.push(task);
}
public:
/** 线程回调
*/
virtual void ThreadCallBack()
{
while (1)
{
{
std::lock_guard lock(m_mutex);
if (m_stop)
{
break;
}
}
// 取出队首
{
std::lock_guard lock(m_mutex);
if (m_taskQue.empty())
{
::Sleep(0);
continue;
}
m_currentTask = m_taskQue.front();
m_taskQue.pop();
if (m_currentTask == nullptr)
{
::Sleep(0);
continue;
}
}
m_currentTask->DoTask();
// 准备销毁
std::shared_ptr tmp = m_currentTask;
{
std::lock_guard lock(m_mutex);
m_currentTask = nullptr;
}
}
}
private:
/** 互斥量
*/
std::recursive_mutex m_mutex;
/** 线程
*/
std::thread* m_pThread;
/** 任务队列
*/
std::queue > m_taskQue;
/** 是否停止了
*/
bool m_stop;
/** 当前正在执行的
*/
std::shared_ptr m_currentTask;
};
需要注意的是:
// 准备销毁
std::shared_ptr tmp = m_currentTask;
{
std::lock_guard lock(m_mutex);
m_currentTask = nullptr;
}
智能指针的置空也需要加锁,如果没有std::shared_ptr
windows中有现成的事件,但是C11没有,上文有说过event有点像生产者消费者模型,C11中的条件变量也很想生产者消费者,所以可以自己实现一个简单事件:
/** 简单事件
@note 不提供析构函数,如果当该类析构的时候,但是还处于Wait() 状态,很不幸的告诉你发生未定义行为
*/
class SimpleEvent :
public NonCopyable
{
public:
/** 等待一个信号
*/
void Wait()
{
std::unique_lock lck(m_mutex);
if (!m_hasSignal)
{
// 不会死锁,考虑:
// 1、m_mutex.unlock();
// 2、wait()
// 3、m_mutex.lock();
// wait(lck) 相当于将1、2两步放到一个cpu周期内
m_con.wait(lck);
}
m_hasSignal = false;
}
/** 设置一个信号
*/
void SetEvent()
{
std::unique_lock lck(m_mutex);
m_hasSignal = true;
m_con.notify_all();
}
private:
/** 互斥量
*/
std::mutex m_mutex;
/** condition_variable
*/
std::condition_variable m_con;
/** 是否有信号,只是记录是否有信号,但是不记录信号次数
@note 主要是为了先调用SetEvent(),然后调用wait的情况。考虑在Wait()前加上::Sleep(1000);
*/
bool m_hasSignal = false;
};
将m_hasSignal 改为int计数将会是一个好用的事件,但是对于任务队列来说,会让队列空转,所以这里使用bool类型标记一下即可。加入event后的代码:
/** 前置声明,简单事件,只SimpleTaskQueue用
*/
class SimpleEvent;
/** 简单线程任务模型
@note 有点像生产者消费者模型,只是消费者只有一个
*/
class SimpleTaskQueue :
public NonCopyable
{
public:
/** 构造函数
*/
SimpleTaskQueue();
/** 析构函数
*/
~SimpleTaskQueue();
public:
/** 开始循环
@return 是否成功
*/
bool Start();
/** 停止所有任务
*/
void StopAll();
/** 停止当前的任务
*/
void StopCurrent();
/** 清空所有任务包括当前的,但不停止循环
*/
void ClearAll();
/** 清空所有任务不包括当前的,但不停止循环
*/
void ClearQueue();
/** 添加一个任务
@param [in] task 任务
*/
void PushTask(const std::shared_ptr& task);
/** 添加一个函数任务
@param [in] task 任务
*/
void PushTask(const std::function& task);
public:
/** 线程回调
*/
virtual void ThreadCallBack();
private:
/** 线程
*/
std::thread* m_pThread;
/** 互斥量
*/
std::recursive_mutex m_mutex;
/** 线程事件
*/
std::shared_ptr m_spEvent;
/** 任务队列
*/
std::queue > m_taskQue;
/** 是否停止了
*/
bool m_stop;
/** 当前正在执行的
*/
std::shared_ptr m_currentTask;
};
#include "SimpleTaskQueue.h"
/** 简单事件
@note 不提供析构函数,如果当该类析构的时候,但是还处于Wait() 状态,很不幸的告诉你发生未定义行为
*/
class SimpleEvent :
public NonCopyable
{
public:
/** 等待一个信号
*/
void Wait()
{
std::unique_lock lck(m_mutex);
if (!m_hasSignal)
{
// 不会死锁,考虑:
// 1、m_mutex.unlock();
// 2、wait()
// 3、m_mutex.lock();
// wait(lck) 相当于将1、2两步放到一个cpu周期内
m_con.wait(lck);
}
m_hasSignal = false;
}
/** 设置一个信号
*/
void SetEvent()
{
std::unique_lock lck(m_mutex);
m_hasSignal = true;
m_con.notify_all();
}
private:
/** 互斥量
*/
std::mutex m_mutex;
/** condition_variable
*/
std::condition_variable m_con;
/** 是否有信号,只是记录是否有信号,但是不记录信号次数
@note 主要是为了先调用SetEvent(),然后调用wait的情况。考虑在Wait()前加上::Sleep(1000);
*/
bool m_hasSignal = false;
};
SimpleTaskQueue::SimpleTaskQueue() :
m_stop(false),
m_pThread(nullptr)
{
}
SimpleTaskQueue::~SimpleTaskQueue()
{
if (m_pThread != nullptr)
{
StopAll();
m_pThread->join();
delete m_pThread;
m_pThread = nullptr;
}
}
bool SimpleTaskQueue::Start()
{
m_spEvent.reset(new(std::nothrow) SimpleEvent());
m_pThread = new (std::nothrow) std::thread(&SimpleTaskQueue::ThreadCallBack, this);
return (m_pThread != NULL) & (m_spEvent != NULL);
}
void SimpleTaskQueue::StopAll()
{
std::lock_guard lock(m_mutex);
ClearAll();
m_stop = true;
m_spEvent->SetEvent();
}
void SimpleTaskQueue::StopCurrent()
{
std::lock_guard lock(m_mutex);
if (m_currentTask != nullptr)
{
m_currentTask->Stop();
}
}
void SimpleTaskQueue::ClearAll()
{
std::lock_guard lock(m_mutex);
StopCurrent();
ClearQueue();
}
void SimpleTaskQueue::ClearQueue()
{
std::lock_guard lock(m_mutex);
std::queue > tmp = std::queue >();
m_taskQue.swap(tmp);
}
void SimpleTaskQueue::PushTask(const std::shared_ptr& task)
{
std::lock_guard lock(m_mutex);
m_taskQue.push(task);
m_spEvent->SetEvent();
}
void SimpleTaskQueue::PushTask(const std::function& task)
{
PushTask(std::shared_ptr(new SimpleFunTask(task)));
}
void SimpleTaskQueue::ThreadCallBack()
{
while (true)
{
{
std::lock_guard lock(m_mutex);
if (m_stop)
{
break;
}
}
m_spEvent->Wait();
// 将队列里的全部执行掉
while (true)
{
// 取出队首
{
std::lock_guard lock(m_mutex);
if (m_taskQue.empty())
{
break;
}
m_currentTask = m_taskQue.front();
m_taskQue.pop();
if (m_currentTask == nullptr)
{
continue;
}
}
// 执行任务
m_currentTask->DoTask();
// 准备销毁
std::shared_ptr tmp = m_currentTask;
{
std::lock_guard lock(m_mutex);
m_currentTask = nullptr;
}
}
}
}
使用示例代码:
void Test_taskQue1()
{
SimpleTaskQueue simpleTaskQue;
simpleTaskQue.Start();
bool ret = false;
int t1 = 0;
int t2 = 0;
simpleTaskQue.PushTask([&]()
{
t1 = 1;
t2 = 2;
ret = true;
});
while (true)
{
::Sleep(1);
}
}
有意思的是
simpleTaskQue.PushTask([&]()
{
t1 = 1;
t2 = 2;
ret = true;
});
可以直接的将lambda中的代码放到线程中执行,非常方便。
代码链接:ddm/ddm/src/thread/task at main · yunate/ddm (github.com)
本文介绍了一个简单任务队列,下文会介绍基于任务队列的一个应用:简单日志