Main thread task queue

Origin

I am used to using Qt, and I also know that Qt can only update the UI interface in the main thread. The various buttons and input in the UI also run in the main thread. But now I am at the wrong time, and the project I am writing has to be run in the embedded system. The embedded memory is inherently small and there is no interface. It is impossible to build a Qt demo for me. I can only use the command line, and I don’t think too much about it. Many, I just started the code and used std::cin and cout in the callback function of the sub-thread to interact. Because it is a multi-threaded environment, the cout output directly changes its shape and is almost out of order. This is caused by multi-threads preempting execution. It is not difficult to do. Just write a WriteLog() function and use a mutex to protect it. As shown below.

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...); 
	}	
};

As smart as I am, out-of-order output can be done just like this. In order not to block the running of the sub-thread, you can also open a separate thread to print logs. You only need to submit each log to the log queue. Unfortunately, I just want a demo. , there is no need to make it so complicated, there is no requirement for performance, it is just to verify that the function is correct. So this version is enough (that is to say, who knows what unimaginable bugs will happen after blocking the sub-line, but I am still quite confident in my code, because each callback is issued by the thread pool, blocking The consequence is that the thread pool queue is getting bigger and bigger. The more messages accumulate, the more messages accumulate,). The output is done, but the input is really difficult for me. Suppose I have two threads A and B, and the two threads call back at the same time, allowing me to input and choose to run the corresponding function. The simplified code is as follows

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;
}

All the problems are exposed in this simple example:

  • Running between multiple threads is out of order
  • The first input in the code may be to update user information, or it may be to update department information.
  • Since I added interference code, std::this_thread::sleep_for(std::chrono::milliseconds(1)); has a high probability of updating some information.
  • I used 1 and 2 for both updates. Even if the order is wrong, the code can still be run, but it will not run according to the prompts. Suppose I use 3 and 4 to update department information, and 1 and 2 to update user information. The input belongs to the update department, but I enter 1 and 2, and any code that updates department information will not be executed. Everything is really unsatisfactory.

So many problems have been exposed one by one, what should we do? But it’s hard for me. I was confused. Fortunately, I had an idea. Why not create a task queue for the main thread? Any input and prompts for input are executed in the main thread. Just like the interface library, all operations on the interface can only be executed in the main thread. It takes place in the main plot, and it's not pretty.

main thread queue

  1. Queue interface overview methods are listed in the following header files
#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
  • This example only stores std::function<void()> as an example. This can already complete most of the work. It can save any function. All functions can be stored in this variable. It only needs to be used when submitting a task. Lambda packaging. This can also be changed to a structure or something to store data. This is just a template and can be ever-changing.
  • Use mutexes for queue read and write protection.
  • Use condition variables to wake up, and use atomic variables to control the exit operation of the queue.
  • Interface design
    • Post submits tasks to the queue, following the first-in-first-out, first-in-first-run principle.
    • Send sends a task to the queue and directly inserts it into the queue to run. This will increase the priority and directly insert it into the front of the queue to run. If there is a task running, then wait, and it must be the next one to run. If it has not been run and there is a new send, then sorry, the new send can only wait and is protected internally by a mutex. Only one send can be run at a time. If multiple sends can be run, there will be a problem with post. What's the difference?
    • Exec This interface is to attach the task queue to the main thread. Then it blocks and waits for the arrival of the task. When the task arrives, it wakes up and runs.
    • Qiut This interface exits the task queue. If the queue is running a task, it will wait for the task to complete before exiting, and will not exit forcibly.
    • They all use static variables or functions, which is equivalent to a single column. This is only used for the main line. After all, there is only one main line. You can remove the static here, and it will be used for any thread. In fact, it is also applicable to any thread now, but the single column mode is so limited. It is very large and can only serve one thread. If you remove static and maintain different real columns by yourself, you can serve different threads, not just the main thread.
  1. accomplish
	#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()
{
    
    
    
}
  • Functions related to templates are written directly into the header file, so there is no need to include cpp. This is how general template files are handled. I won’t go into details here.
  • The first is the exec interface, which uses condition variables to wake up. If there are tasks in the queue and the queue needs to be stopped, it will wake up immediately. Then remove the task from the queue and make the call. View the cpp file in detail.
  • The quit interface is very simple. You only need to set the atomic variable m_stop to true, and then notify the thread that it is time to wake up and process the task. too easy.
  • Then there is the task interface to add. This task interface is very important. The send task is inserted first, the post task is inserted last, and then an expectation is returned. You can get the function return value. post returns the expected value. You need to call get to wait for the expectation to be ready to obtain the value, and send internally waits for the expectation to obtain the value. And using condition variables for protection, only one thread in a multi-thread thread can access it. Use std::packaged_task to encapsulate the passed function into a callable object. Put it into the lambda and store it in the queue. Then notify the thread to prepare to take the task and run it. For the specific implementation, see TaskQueue::AddTask(bool front, F&& f, Args&&… args)->… After a few lines of code, you can understand it at a glance. Ah ha ha. ^_^
  1. test 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;
   
}
  • The demo is very simple. Create a sub-thread to submit and send tasks to the main thread, and then exit the main thread queue. The program has finished running. The running results are as follows:
    Insert image description here

Summarize

  • The thread queue is not only used for the main thread, but can be used for any thread. For example, in SDK, a sub-thread is often created to be the main thread of the SDK.
  • At the same time, you can also start a thread directly, which is very convenient to use.
std::thread __run(&TaskQueue::Exec);
__run.detach();
  • Thread queue and message queue are very similar, such as windows: SendMessage PostMessage, Qt: :sendEvent postEvent, post is submitted to the thread, regardless of his life or death, send has to wait for execution (their implementation must be quite complicated, How can I explain it clearly in a few words? I haven’t studied it in detail. I just memorized the eight-part essay and tried it on the back. I just know its function. It feels a bit like thread join detach)
  • Sometimes when I think about the problem in another direction, I feel that I will find a new world. The main thread task queue is completely inspired by the thread pool's safety queue. In the past, I would only submit tasks from the main thread to sub-threads and sub-threads to sub-threads. I never thought about sub-thread submission. What will happen if the task goes to the main thread.

Guess you like

Origin blog.csdn.net/qq_33944628/article/details/124831922