[23 Autumn Recruitment C++ Backend Interview Technology Breakthrough] The Implementation Principle of C++11 Thread Pool and the Use of Callback Function

About the thread pool

Simply put, there are a bunch of threads that have been created (the maximum number is certain), and they are all idle at the beginning. When a new task comes in, an idle thread is taken from the thread pool to process the task, and when the task is processed, the thread is put back into the thread pool for use by other tasks. When the threads in the thread pool are all processing tasks, there are no idle threads for use. At this time, if a new task is generated, it can only be executed when there is a thread in the thread pool to finish and the task is idle.

Advantages of thread pool

Threads are inherently reusable resources and do not need to be initialized each time they are used. Therefore, a limited number of threads can be used to handle unlimited tasks. It can not only improve speed and efficiency, but also reduce the overhead of frequent thread creation. For example, if you want to do work asynchronously, there is no need to wait. Throw it into the thread pool for processing, and the result is processed in the callback. Frequently executed asynchronous tasks, if threads are created every time, it will inevitably cause a lot of overhead. For asynchronous tasks that are frequently executed in java, just new Therad{}.start(), and then ignore it is not a good way. Frequent calls may trigger GC and cause serious performance problems. Threads should be used for similar tasks pool.

For example, if all computing tasks are carried out on the main thread, then the processing flow of the main thread will inevitably be blocked, and real-time processing cannot be achieved. Using multithreading technology is a solution that everyone naturally thinks of. In the above scenario, threads will be created and destroyed frequently, and such overhead is believed to be unacceptable. At this time, thread pool technology is a good choice.

In addition, in some high-concurrency network applications, the thread pool is also a commonly used technology. The C++ multi-threaded server programming model recommended by Chen Shuo is: one loop per thread + thread pool. Usually, there will be a separate thread responsible for accepting requests from clients, and after parsing the requests, submit the data processing tasks to a dedicated Compute thread pool.

Realization principles and ideas

The general principle is to create a class to manage a task queue and a thread queue. Then each time a task is assigned to a thread to do it, the cycle repeats. The task queue is responsible for storing the tasks that the main thread needs to process. The worker thread queue is actually an infinite loop, responsible for taking out and running tasks from the task queue, which can be regarded as a model of a producer and multiple consumers.

Although c++11 has added the thread library thread, c++'s support for multithreading is still relatively low-level, and slightly more advanced usage needs to be implemented by yourself. There is also a highly anticipated network library, which has not been supported in the standard library so far. Commonly used asio instead. Thanks to the dedication of the great gods on the Internet, the source code is pasted here and the usage method is improved, mainly adding usage examples and the use of callback functions.

There are supporting materials and source code, click here to get

Example of use

#include <iostream>
#include <chrono>
#include <thread>
#include <future>
#include "threadpool.h"
using namespace std;
using namespace std::chrono;

//仿函数示例
struct gfun {
  int operator()(int n) {
    printf("%d  hello, gfun !  %d\n" ,n, std::this_thread::get_id() );
    return 42;
  }
};

class A { 
  public:
    static std::string Bfun(int n, std::string str, char c) {
      std::cout << n << "  hello, Bfun !  "<< str.c_str() <<"  " << (int)c <<"  " << std::this_thread::get_id() << std::endl;
      return str;
    }
};


int main() {

  cout << "hello,this is a test using threadpool" <<endl;

  me::ThreadPool pool(4);
  std::vector< std::future<int> > results;

  //lambada表达式 匿名函数线程中执行
  pool.commit([] {
      std::cout << "this is running in pool therad " << std::endl;
      std::this_thread::sleep_for(std::chrono::seconds(1));
      });

  //仿函数放到线程池中执行
  std::future<int> fg = pool.commit(gfun{},0);  

  std::future<std::string> gh = pool.commit(A::Bfun, 999,"mult args", 123);
  //回调函数示例,模拟耗时操作,结果回调输出
  auto fetchDataFromDB = [](std::string recvdData,std::function<int(std::string &)> cback) {
    // Make sure that function takes 5 seconds to complete
    std::this_thread::sleep_for(seconds(5));
    //Do stuff like creating DB Connection and fetching Data
    if(cback != nullptr){
      std::string out = "this is from callback ";
      cback(out);
    }
    return "DB_" + recvdData;
  };

  //模拟,回调
  fetchDataFromDB("aaa",[&](std::string &result){
      std::cout << "callback result:" << result << std::endl;
      return 0;
      } );

  //把fetchDataFromDB这一IO耗时任务放到线程里异步执行
  //
  std::future<std::string> resultFromDB = std::async(std::launch::async, fetchDataFromDB, "Data0",
      [&](std::string &result){
      std::cout << "callback result from thread:" << result << std::endl;
      return 0;
      }); 


  //把fetchDataFromDB这一IO耗时操作放到pool中的效果
  pool.commit(fetchDataFromDB,"Data1",[&](std::string &result){
      std::cout << "callback result from pool thread:" << result << std::endl;
      return 0;
      });


  for(int i = 0; i < 8; ++i) {
    results.emplace_back(
        pool.commit([i] {
          std::cout << "hello " << i << std::endl;
          std::this_thread::sleep_for(std::chrono::seconds(1));
          std::cout << "world " << i << std::endl;
          return i*i;
          })
        );
  }

  for(auto && result: results){
    std::cout << result.get() << ' ';
  }
  std::cout << std::endl;
}

The following is the specific implementation process:

#pragma once
#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <vector>
#include <queue>
#include <atomic>
#include <future>
//#include <condition_variable>
//#include <thread>
#include <functional>
#include <stdexcept>

namespace me
{
  using namespace std;
//线程池最大容量,应尽量设小一点
#define  THREADPOOL_MAX_NUM 16
//#define  THREADPOOL_AUTO_GROW

//线程池,可以提交变参函数或拉姆达表达式的匿名函数执行,可以获取执行返回值
//不直接支持类成员函数, 支持类静态成员函数或全局函数,Opteron()函数等
class ThreadPool
{
    using Task = function<void()>;    //定义类型
    vector<thread> _pool;     //线程池
    queue<Task> _tasks;            //任务队列
    mutex _lock;                   //同步
    condition_variable _task_cv;   //条件阻塞
    atomic<bool> _run{ true };     //线程池是否执行
    atomic<int>  _idlThrNum{ 0 };  //空闲线程数量

public:
    inline ThreadPool(unsigned short size = 4) { addThread(size); }
    inline ~ThreadPool()
    {
        _run=false;
        _task_cv.notify_all(); // 唤醒所有线程执行
        for (thread& thread : _pool) {
            //thread.detach(); // 让线程“自生自灭”
            if(thread.joinable())
                thread.join(); // 等待任务结束, 前提:线程一定会执行完
        }
    }

public:
    // 提交一个任务
    // 调用.get()获取返回值会等待任务执行完,获取返回值
    // 有两种方法可以实现调用类成员,
    // 一种是使用   bind: .commit(std::bind(&Dog::sayHello, &dog));
    // 一种是用   mem_fn: .commit(std::mem_fn(&Dog::sayHello), this)
    template<class F, class... Args>
    auto commit(F&& f, Args&&... args) ->future<decltype(f(args...))>
    {
        if (!_run)    // stoped ??
            throw runtime_error("commit on ThreadPool is stopped.");

        using RetType = decltype(f(args...)); // typename std::result_of<F(Args...)>::type, 函数 f 的返回值类型
        auto task = make_shared<packaged_task<RetType()>>(
            bind(forward<F>(f), forward<Args>(args)...)
        ); // 把函数入口及参数,打包(绑定)
        future<RetType> future = task->get_future();
        {    // 添加任务到队列
            lock_guard<mutex> lock{ _lock };//对当前块的语句加锁  lock_guard 是 mutex 的 stack 封装类,构造的时候 lock(),析构的时候 unlock()
            _tasks.emplace([task](){ // push(Task{...}) 放到队列后面
                (*task)();
            });
        }
#ifdef THREADPOOL_AUTO_GROW
        if (_idlThrNum < 1 && _pool.size() < THREADPOOL_MAX_NUM)
            addThread(1);
#endif // !THREADPOOL_AUTO_GROW
        _task_cv.notify_one(); // 唤醒一个线程执行

        return future;
    }

    //空闲线程数量
    int idlCount() { return _idlThrNum; }
    //线程数量
    int thrCount() { return _pool.size(); }
#ifndef THREADPOOL_AUTO_GROW
private:
#endif // !THREADPOOL_AUTO_GROW
    //添加指定数量的线程
    void addThread(unsigned short size)
    {
        for (; _pool.size() < THREADPOOL_MAX_NUM && size > 0; --size)
        {   //增加线程数量,但不超过 预定义数量 THREADPOOL_MAX_NUM
            _pool.emplace_back( [this]{ //工作线程函数
                while (_run)
                {
                    Task task; // 获取一个待执行的 task
                    {
                        // unique_lock 相比 lock_guard 的好处是:可以随时 unlock() 和 lock()
                        unique_lock<mutex> lock{ _lock };
                        _task_cv.wait(lock, [this]{
                                return !_run || !_tasks.empty();
                        }); // wait 直到有 task
                        if (!_run && _tasks.empty())
                            return;
                        task = move(_tasks.front()); // 按先进先出从队列取一个 task
                        _tasks.pop();
                    }
                    _idlThrNum--;
                    task();//执行任务
                    _idlThrNum++;
                }
            });
            _idlThrNum++;
        }
    }
};

}

#endif  //https://github.com/lzpong/

Reference Blog Intrusion and Deletion: The Implementation Principle of C++11 Thread Pool and the Use of Callback Function_Maverick Cat A Yang Yongzhen's Technology Blog_51CTO Blog

Irregularly update the content related to Linux C/C++ background architecture development; there are also updated examples on the homepage of the Lion homepage for the architecture system learning route; friends who want to learn must not miss it; relevant learning materials and supporting teaching materials can be obtained here :

Guess you like

Origin blog.csdn.net/m0_58687318/article/details/127129575