主线程任务队列

由来

平时用惯了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,那更新部门信息的任何代码都不会被执行。真的是万事不尽人意啊。

这么多问题一一暴露了出来,这如何是好啊。可把我难的。头大,还好我灵光一现,为何不搞个主线程的任务队列,任何输入和提示输入的内容都放在主线程中执行,就像界面库那样,所有关于界面的操作都只能在主线中进行,且不美哉。

主线程队列

  1. 队列接口概况 方法列入下列头文件中
#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 自己维护不同的实列,那么可以为不同的线程服务,不仅仅局限于主线程。
  1. 实现
	#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)->… 了了数行代码,一眼即懂。啊哈哈。^_^
  1. 测试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有点像)
  • 有时候换个方向思考问题,感觉会发现新大陆,主线程任务队列灵感完全来源于线程池得安全队列,以前只会从主线提交任务到子线程、子线程到子线,就没想过子线程提交任务到主线程会如何。

猜你喜欢

转载自blog.csdn.net/qq_33944628/article/details/124831922