基於C++11的執行緒池實現

雲夢士 發表於 2022-05-10
C++

1.執行緒池

1.1 執行緒池是什麼?

一種執行緒管理方式。

1.2 為什麼用執行緒池?

執行緒的建立和銷燬都需要消耗系統開銷,當執行緒數量過多,系統開銷過大,就會影響快取區域性性和整體效能。而執行緒池能夠在充分利用核心資源的前提下,避免系統資源被過度呼叫。

1.3 如何設計執行緒池?

簡單來說,線上程池中提前建立好多個執行緒,使用時從執行緒池中取出,使用完放回執行緒池。執行緒池中的執行緒排程由執行緒池中的管理者執行緒排程。

2.基於C++11的實現

Talk is cheap. Show me the code.

直接看程式,原理、函式在後面再介紹。

2.1 程式

程式主要分為四個檔案,分別為:

  • Task.h //任務類
  • ThreadPool.h //執行緒池類
  • ThreadPool.cpp //執行緒池類實現
  • main.cpp //測試程式

2.1.2 任務類Task.h

#pragma once
using callback = void(*)(void*);//函式指標,定義別名

class Task{
public:
    callback func;//回撥任務函式
    void* arg;    //函式引數
public:
    Task() {                        //無參建構函式
        this->func = nullptr;
        this->arg = nullptr;
    }
    Task(callback func, void* arg) {//含參建構函式
        this->func = func;
        this->arg = arg;
    }
    ~Task() = default;              //解構函式
    Task(const Task &t) = default;  //拷貝建構函式
    Task& operator=(const Task &t); //拷貝賦值操作符
    Task(Task &&t) = default;       //移動建構函式,注意不能有const
    Task& operator=(const Task &&t);//移動賦值操作符
};

2.1.2 執行緒池類ThreadPool.h

#pragma once

#include "Task.h"
#include <thread>
#include <queue>
#include <vector>
#include <atomic>
#include <mutex>
#include <condition_variable>

using namespace std;

class ThreadPool {
public:
    ThreadPool(int minSize, int maxSize);//建構函式
    void AddTask(Task task);             //新增新任務
    int GetBusyNum();                    //獲取當前工作中的執行緒數
    int GetAliveNum();                   //獲取當前活著的執行緒數
    int GetTaskQueueSize();              //獲取當前任務佇列長度
    ~ThreadPool();

    ThreadPool(const ThreadPool &t) = default;  //拷貝建構函式
    ThreadPool& operator=(const ThreadPool &t); //拷貝賦值操作符
    ThreadPool(ThreadPool &&t) = default;       //移動建構函式
    ThreadPool& operator=(const ThreadPool &&t);//移動賦值操作符


private:
    queue<Task> taskQueue; //任務佇列
    thread managerID;//管理者執行緒ID
    vector<thread> threadIDs;//工作中的執行緒組ID
    int minNum;//最小執行緒數量(如果執行緒池中執行緒的數目過少,處理器的一些核可能就無法充分利用,浪費)
    int maxNum;//最大執行緒數量(如果執行緒池中執行緒的數量過多,最終它們會競爭稀缺的處理器和記憶體資源,浪費大量的時間在上下文切換上。)
    atomic_int busyNum;//工作中的執行緒數量(atomic_int保證其賦值,取值操作的原子性)
    atomic_int liveNum;//活著的執行緒數量
    atomic_int exitNum;//將要被銷燬的執行緒數量

    mutex mutexPool;//執行緒池的鎖
    condition_variable cond;//條件變數
    bool shutDown;//是不是要銷燬執行緒池, 銷燬為true, 不銷燬為false

    static void worker(void* arg);//工作的執行緒任務函式
    static void manager(void* arg);//管理者執行緒任務函式

    static const int NUMBER = 2;//管理者執行緒每次增加/銷燬的執行緒數
};

2.1.3 執行緒池類實現ThreadPool.cpp

#include "ThreadPool.h"
#include <unistd.h> //pthread_self
#include <iostream>

using namespace std;

ThreadPool::ThreadPool(int minSize, int maxSize) {
    do{
        minNum = minSize;
        maxNum = maxSize;
        busyNum = 0;
        liveNum = minSize;
        exitNum = 0;
        shutDown = false;

        //初始化管理者執行緒和工作執行緒組
        managerID = thread(manager, this);
        threadIDs.resize(maxSize);
        for(int i = 0; i < minSize; ++i) {
            threadIDs[i] = thread(worker, this);
        }

        return;
    } while(0);//do{...}while(0)結構提高程式碼健壯性
}

ThreadPool::~ThreadPool() {
    shutDown = true;
    if(managerID.joinable()) {//阻塞在管理者執行緒,直到其執行完,再向下進行
        managerID.join();
    }
    cond.notify_all();//喚醒所有等待的執行緒
    for(int i = 0; i < maxNum; ++i) {//依次執行工作者的執行緒
        if(threadIDs[i].joinable()) {
            threadIDs[i].join();
        }
    }
}

//新增新任務
void ThreadPool::AddTask(Task task) {
    unique_lock<mutex> poolLock(mutexPool);
    if(shutDown) {
        return;
    }
    taskQueue.emplace(task);
    cond.notify_all();
}

int ThreadPool::GetBusyNum() {
    return busyNum;
}

int ThreadPool::GetAliveNum() {
    return liveNum;
}

int ThreadPool::GetTaskQueueSize() {
    unique_lock<mutex> poolLock(mutexPool);
    int queueSize = taskQueue.size();
    poolLock.unlock();
    return queueSize;
}

//工作者執行緒
void ThreadPool::worker(void* arg) {
    ThreadPool* pool = static_cast<ThreadPool*>(arg);
    while(true) {
        unique_lock<mutex> poolLock(pool->mutexPool);
        //若當前任務佇列為空且執行緒池處於開啟狀態
        while(pool->taskQueue.empty() && !pool->shutDown) {
            pool->cond.wait(poolLock);//阻塞工作執行緒
            //若存在待銷燬執行緒
            if(pool->exitNum > 0) {
                --pool->exitNum;
                if(pool->liveNum > pool->minNum) {//若活著的執行緒數大於最小執行緒數,則可以進行銷燬
                    --pool->liveNum;
                    cout << "threadID: " << pthread_self() << " has exited." << endl;
                    return;
                }
            }
        }

        //判斷執行緒池是否關閉了
        if(pool->shutDown) {
            cout << "threadID: " << pthread_self() << " has exited." << endl;
            return;
        }

        //從任務佇列中取出一個任務
        Task task = pool->taskQueue.front();
        pool->taskQueue.pop();
        ++pool->busyNum;

         //解鎖
        poolLock.unlock();

        //執行任務
        cout << "threadID: " << pthread_self() << " start to work." << endl;
        task.func(task.arg);
        task.arg = nullptr;

        //執行完後,工作執行緒數-1
        cout << "threadID: " << pthread_self() << " stop working." << endl;
        --pool->busyNum;
    }
}

//管理者執行緒
void ThreadPool::manager(void* arg) {
    ThreadPool* pool = static_cast<ThreadPool*>(arg);
    while(!pool->shutDown) {
        //每隔3秒檢測一次
        sleep(3);

        //新增新執行緒
        //若任務個數大於活著的執行緒數,且活著的執行緒數小於最大執行緒數
        if(pool->GetTaskQueueSize() > pool->liveNum && pool->liveNum < pool->maxNum) {
            unique_lock<mutex> poolLock(pool->mutexPool);
            poolLock.lock();
            int count = 0;
            for(int i = 0; i < pool->maxNum && count < ThreadPool::NUMBER && pool->liveNum < pool->maxNum; ++i) {
                if(pool->threadIDs[i].get_id() == thread::id()) {
                    cout << "Create a new thread." << endl;
                    pool->threadIDs[i] = thread(worker, pool);
                    ++count;
                    ++pool->liveNum;
                }
            }
            poolLock.unlock();
        }

        //銷燬執行緒
        //若忙的執行緒*2小於存活的執行緒數,且存活的執行緒數大於最小的執行緒數
        if(pool->busyNum * 2 < pool->liveNum && pool->liveNum > pool->minNum) {
            pool->exitNum = ThreadPool::NUMBER;
            for(int i = 0; i < ThreadPool::NUMBER; ++i) {//讓工作的執行緒自殺
                pool->cond.notify_all();
            }
        }
    }
}

2.2 測試方法:

將上述檔案放在Linux下的一個資料夾(我這裡是\Share\study_threadPool\myself)

  • 進入該資料夾:cd /share/study_threadPool/myself/
  • 編譯:g++ main.cpp ThreadPool.cpp -o ThreadPool.o -pthread
  • 執行:./ThreadPool.o

2.2 C++11相關函式

  1. thread類
  • ThreadPool.cpp第17行:managerID = thread(manager, this);表示建立一個新執行緒,manager是該執行緒執行的函式,this是該執行緒執行函式的引數。
  • ThreadPool.cpp第29行:managerID.joinable() 判斷該執行緒是否可以join
  • ThreadPool.cpp第30行:managerID.join() 阻塞在該執行緒,直到其執行完
  • ThreadPool.cpp第123行:pool->threadIDs[i].get_id()表示獲取該執行緒的ID
  1. mutex
  • ThreadPool.cpp第42行:unique_lock<mutex> poolLock(mutexPool); 自動加鎖與解鎖
  • ThreadPool.cpp第61行:poolLock.unlock();解鎖
  • ThreadPool.cpp第120行:poolLock.lock();加鎖
  1. condition_variable
  • ThreadPool.cpp第32行:nd.notify_all();喚醒所有等待的執行緒
  1. atomic
  • ThreadPool.h第34行:atomic_int busyNum;本質還是int,只是每次對其操作時,都能保證是原子操作
  1. using
  • Task.h第2行:sing callback = void(*)(void*);函式的別名

3.除錯過程中出現的問題及解決方法

3.1 warning:#pragma once in main file

image
解決方案:g++編譯時不要編譯標頭檔案

3.2 移動建構函式出錯

image
解決方案:移動建構函式的引數不能加const

4.參考