由来
平时用惯了qt,也知道qt只能在主线程中更新ui界面,ui中的各种按钮,输入也是在主线程中运行的。但如今生不逢时,所写项目竟要在嵌入式中运行,嵌入式内存本来就小,也没有界面,不可能给我搞个qt demo吧,只能使用命令行了,也没想太多,直接就撸起了代码,在子线程的回调函数中使用std::cin、cout 进行交互。由于是多线程环境,cout输出直接变了型,几乎是乱序输出,这是由于多线程抢占执行所致,也不难搞,直接写个WriteLog()函数,使用互斥锁保护起来,就如下面那样。
class Utility
{
public:
// c++11 风格
static void WriteLog()
{
std::cout << '\n';
}
template<typename T, typename ... Args>
static void WriteLog(const T& fristArg, const Args& ...args)
{
static std::mutex logMutex;
std::lock_guard<std::mutex> lk(logMutex);
std::cout << fristArg;
// c++17起 不需要无参递归函数,也不需要第一个参数,直接(std::cout << ... << args)<< std::endl;
WriteLog(args...);
}
};
机智如我,乱序输出也就这样搞好了,为了不阻塞子线程运行,还可以单独开启一个线程来打印日志,只需将每个日志提交到日志队列即可,可惜我只是要个demo,没必要搞这么复杂,对性能毫无要求,仅仅只是验证功能正确即可。所以此版本即可(话是这样说,鬼知道阻塞子线之后会发生什么难以想象的bug,不过我对我的代码还是挺有信心,由于每个回调都是由线程池发出的,阻塞的后果就是线程池队列越来越大。消息越积累越多,)。输出是搞定了,可是输入确实难到我了,假如我有A、B两个线程,两个线程同时回调,让我输入选择运行相应的功能,大体简化之后代码如下那样
void UpdateUser()
{
Utility::WriteLog("UpdateUser: ","1 跟新自己的信息, 2 更新别人的信息");
int i = 0;
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 制造乱序环境
std::cin >> i;
if (i == 1)
{
Utility::WriteLog("UpdateUser: 正在更新自己信息");
}
else
{
Utility::WriteLog("UpdateUser: 正在更新别人信息");
}
}
void UpdateDepartment()
{
Utility::WriteLog("UpdateDepartment:", "1 更新界面, 2 保存到数据库 ");
int i = 0;
std::cin >> i;
if (i == 1)
{
Utility::WriteLog("UpdateDepartment: 正在更新界面");
}
else
{
Utility::WriteLog("UpdateDepartment: 正在保存数据库...");
}
}
int main()
{
auto joba = std::thread([] {
UpdateUser();
});
auto jobb = std::thread([] {
UpdateDepartment();
});
joba.join();
jobb.join();
return 0;
}
就这个简单的小例子所有的问题都暴露出来了:
- 多线程之间运行是乱序的
- 代码中第一个输入有可能是更新用户信息,有可能是更新部门信息的,
- 由于我加入干扰代码,std::this_thread::sleep_for(std::chrono::milliseconds(1)); 有大概率是更新部门信息。
- 两处更新我都是用1 、2 即使顺序错误代码还是可以运行的,只是不会按照提示输入的来运行。那假设我更新部门信息用3 和4,更新用户信息用1、2,那输入是属于更新部门的,我却输入了1、2,那更新部门信息的任何代码都不会被执行。真的是万事不尽人意啊。
这么多问题一一暴露了出来,这如何是好啊。可把我难的。头大,还好我灵光一现,为何不搞个主线程的任务队列,任何输入和提示输入的内容都放在主线程中执行,就像界面库那样,所有关于界面的操作都只能在主线中进行,且不美哉。
主线程队列
- 队列接口概况 方法列入下列头文件中
#ifndef _safe_queue_h
#define _safe_queue_h
#include <vector>
#include <deque>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>
#include <atomic>
class TaskQueue {
public:
TaskQueue();
~TaskQueue();
template<class F, class... Args>
static auto Post(F&& f, Args&&... args)-> std::future<typename std::result_of<F(Args...)>::type>;
template<class F, class... Args>
// 如果消息队列中没有任务立即运行,有任务将在下一次运行 以最近一次send为基准
static auto Send(F&& f, Args&&... args)-> typename std::result_of<F(Args...)>::type;
static void Exec();
static void Qiut();
private:
template<class F, class... Args>
static auto AddTask(bool front, F&& f, Args&&... args)-> std::future<typename std::result_of<F(Args...)>::type>;
private:
static std::deque<std::function<void()>> m_tasks;
static std::mutex m_mtx;
static std::condition_variable m_condition_var;
static std::atomic_bool m_stop;
};
template<class F, class... Args>
auto TaskQueue::Post(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
return AddTask(false, std::forward<F>(f), std::forward<args>(args)...);
}
template<class F, class... Args>
auto TaskQueue::Send(F&& f, Args&&... args)
-> typename std::result_of<F(Args...)>::type
{
static std::mutex mutex; // 一次只能有一个线程使用该函数,多个线程只能等待,这是规则
std::lock_guard<std::mutex> lk(mutex);
return AddTask(true, std::forward<F>(f), std::forward<args>(args)...).get(); // 必须等待函数
}
template<class F, class... Args>
auto TaskQueue::AddTask(bool front, F&& f, Args&&... args)-> std::future<typename std::result_of<F(Args...)>::type>
{
using return_type = typename std::result_of<F(Args...)>::type;
auto task = std::make_shared<std::packaged_task<return_type()> >(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(m_mtx);
if(m_stop)
{
throw std::runtime_error("task queue quited ...");
}
if(front)
{
m_tasks.emplace_front([task]{ (*task)(); });
}
else
{
m_tasks.emplace_back([task]{ (*task)(); });
}
}
m_condition_var.notify_one();
return res;
}
#endif
- 该列子仅仅举例存储 std::function<void()> ,这个已经能完成大部分工作了,他可以保存任何一切的函数,一切函数都可以存在该变量中,仅仅只需要在提交任务的时候使用lamda包装一下。这也可以改为结构体啥的,存储数据,这仅仅是一个模板,可以千变万化。
- 使用互斥量进行队列读写保护。
- 使用条件变量进行唤醒,同时使用原子变量控制队列的退出操作。
- 接口设计
- Post 提交任务到队列中,遵循先进先出、先进先运行的原则
- Send 发送任务到队列中,直接插队运行,会提高优先级,直接插入到队首运行,如果有任务正在运行,那么等待着,下一个一定是他在运行。如若他没有被运行,又有新的send , 那不好意思,新的shend只能等待,内部使用互斥量来保护,一次只能运行一个send, 如果可以运行多个send, 那和post又有什么区别呢?
- Exec 该接口是将任务队列附加到主线程上。然后阻塞等待任务的到来,任务到来马上唤醒,进行运行。
- Qiut 该接口退出任务队列,如果队列正在运行一个任务,那么会等任务运行完毕后在退出,并不会强行退出,
- 均采用static 变量或者函数,相当于单列,这只是用于主线,毕竟主线也只有一个,这儿可以将static去掉,那么将是用于任何线程,其实现在也适用任何线程,但是单列模式,所以限制很大,仅仅只能为一个线程服务,去掉static 自己维护不同的实列,那么可以为不同的线程服务,不仅仅局限于主线程。
- 实现
#include "TaskQueue.h"
std::deque<std::function<void()>> TaskQueue::m_tasks;
std::mutex TaskQueue::m_mtx;
std::condition_variable TaskQueue::m_condition_var;
std::atomic_bool TaskQueue::m_stop;
TaskQueue::TaskQueue()
{
m_stop.store(false);
}
void TaskQueue::Exec()
{
m_stop.store(false);
for (;;)
{
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(m_mtx);
m_condition_var.wait(lock,
[] { return m_stop || !m_tasks.empty(); });
if (m_stop && m_tasks.empty())
{
break;
}
task = std::move(m_tasks.front());
m_tasks.pop_front();
}
task();
}
}
void TaskQueue::Qiut()
{
m_stop.store(true);
m_condition_var.notify_one();
}
TaskQueue::~TaskQueue()
{
}
- 涉及到模板的函数是直接写到头文件中, 这样就不需要包含cpp了,一般模板文件都这么处理。这里就不在细说了。
- 首先是exec接口,该接口使用条件变量唤醒,如果队列中有任务和要停止队列都马上唤醒。然后从队列中取出任务,进行调用。详细查看cpp文件。
- quit 接口就很简单了,仅仅需要把原子变量 m_stop设置为true,然后通知线程该醒来处理任务了,即可。太简单了。
- 然后就是增加任务的接口了,该任务接口十分重要,将send任务进行首插,post任务进行尾插,然后返回一个期望。可以获取函数返回值。 post是返回的是期望值。需要调用get等待期望准备就绪获取值,send就内部等待期望获取值。并且采用条件变量进行保护,多线线程中仅仅只可以有一个线程访问得到。使用std::packaged_task 把传入的函数封装为可调用对象。放入lamda中,并存进队列。然后通知线程准备取任务运行。具体实现看TaskQueue::AddTask(bool front, F&& f, Args&&… args)->… 了了数行代码,一眼即懂。啊哈哈。^_^
- 测试demo
#include "TaskQueue.h"
int addJo(int a, int b)
{
printf("addJo is run ...\n");
TaskQueue::Post([=]{
printf("post: %d + %d = %d\n", a, b , a + b);
});
TaskQueue::Send([=]{
printf("send: %d + %d = %d\n", a, b , a + b);
});
printf(" quit main thread task queue\n");
TaskQueue::Qiut();
}
void test()
{
while(true)
{
static int index = 0;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
if(++index == 10)
{
addJo(1, 2);
break;
}
printf("test thread run index: %d\n", index);
}
}
int main()
{
std::thread th(&test);
th.detach();
TaskQueue::Exec();
return 0;
}
- demo 很简单,开辟一个子线程来提交、发送任务到主线程中,然后退出主线程队列。程序运行完毕。运行结果如下图:
总结
- 线程队列不仅仅用于主线程,可以使用于任何线程,比如sdk中,往往就会创建一个子线程来做sdk的主线程。
- 同时也可以直接启动一个线程,用起来挺方便的。
std::thread __run(&TaskQueue::Exec);
__run.detach();
- 线程队列和消息队列十分的相似,比如windows: SendMessage PostMessage, Qt: :sendEvent postEvent,post 就是提交到线程中去,不管他的死活,send 就得等代执行(他们的实现肯定应该是相当复杂,怎是三言两语能说清楚得,我也没有具体去研究过,就背背面试用的八股文,知道其功能而已,感觉和thread join detach有点像)
- 有时候换个方向思考问题,感觉会发现新大陆,主线程任务队列灵感完全来源于线程池得安全队列,以前只会从主线提交任务到子线程、子线程到子线,就没想过子线程提交任务到主线程会如何。