線程池是一種很經典的技術,在后端系統中很常見。線程池的常規做法是提前創建好一組工作線程,然后將任務分發給這些工作線程來處理,這樣就避免了頻繁的線程創建和銷毀,同時也能很好的控制線程數量。線程池本質上是一種池化技術,利用空間來換取時間。線程池技術已經存在很多年,在面試的時候被問到的概率很高,在工作中也非常有用。
首先來看面試中的線程池,通常面試官會提問線程池的目的和原理,如果面試時間充足的話,恭喜你可能要進入緊張刺激的“白紙編程”(又叫“白板面試”,在一張A4紙上手寫代碼)階段了。
線程池在設計和實現時主要考慮“任務”和“工作線程”之間的協作關系。通常我們把線程池創建的工作線程稱之為worker線程,他們就像一群任勞任怨、勤勞無比的工人們(like you and me)一樣,等着有人給安排活兒干或主動找任務做;任務通常被抽象成一個類,主要提供一種統一、通用的任務接口,以便線程池中的worker線程進行無差別的調用。
下面的代碼是一個簡單任務Task類的例子,簡單起見,這個類只提供了一個沒有返回值和參數的抽象方法Run。
1 define _THREAD_POOL_H 2
3 #include <pthread.h>
4 #include <iostream>
5 #include <string>
6 #include <list>
7
8 namespace thread { 9
10 const int MIN_THREAD_NUM = 4; 11 const int MAX_THREAD_NUM = 100; 12
13 // 任務類接口,只提供一個Run方法
14 class Task { 15 public: 16 virtual void Run() = 0; 17 virtual ~Task() {} 18 };
線程池通常提供兩個接口Init和AddTask,其中Init接口用來初始化線程池資源,比如創建指定數目的worker線程,以及初始化任務隊列,任務隊列用來保存用戶添加的各種任務,由於任務隊列主要涉及添加和刪除操作,因此STL中的list容器比較適合;AddTask接口是調用最多的接口,用於向線程池中添加任務。用戶添加任務和worker線程獲取任務這兩個操作需要加互斥鎖來保護任務隊列,下面的代碼是線程池類ThreadPool。
// 線程池
class ThreadPool {
public:
ThreadPool() : max_thread_num_(0) {}
// 初始化線程池,同時設置最大worker線程個數
bool Init(int max_thread_num);
// 添加任務到線程池
void AddTask(Task *task);
private:
static void *StartWorker(void *argv);
void Do();
private:
int max_thread_num_;
pthread_mutex_t lock_;
pthread_cond_t cond_;
std::list<Task*> task_list_; // 任務隊列
};
} // namespace thread
#endif
這里解釋下線程池中的StartWorker函數為什么被設計成static靜態函數,這是由pthread_create的線程入口參數必須是靜態函數這個限制條件決定的。同時由於我們只需要給用戶提供Init和AddTask接口,所以StartWorker函數被設計成私有的。
下面的代碼是線程池類ThreadPool的實現,作為一個示例程序,這里的函數調用都沒有判斷返回值。
#include "thread_pool.h"
#include <cstdio>
namespace thread {
bool ThreadPool::Init(int max_thread_num) {
// 參數合法性檢查
if (max_thread_num < MIN_THREAD_NUM ||
max_thread_num > MAX_THREAD_NUM) {
printf("Error: Invalid parameter thread number:%d\n",
max_thread_num);
return false;
}
//初始化鎖、條件變量
pthread_mutex_init(&lock_, NULL);
pthread_cond_init(&cond_, NULL);
pthread_t thd;
for (int i = 0; i < max_thread_num; ++i) {
// 創建線程
// 注意StartWorker的參數是this指針,即ThreadPool*類型指針
pthread_create(&thd, NULL, ThreadPool::StartWorker, this);
}
max_thread_num_ = max_thread_num;
return true;
}
void ThreadPool::AddTask(Task *task) {
if (task == NULL) {
return;
}
pthread_mutex_lock(&lock_);
task_list_.push_back(task);
pthread_mutex_unlock(&lock_);
pthread_cond_signal(&cond_);
}
void *ThreadPool::StartWorker(void *argv) {
ThreadPool *pool = reinterpret_cast<ThreadPool*>(argv);
pool->Do();
return NULL;
}
void ThreadPool::Do() {
// worker線程處理循環
while (true) {
// 等待任務
pthread_mutex_lock(&lock_);
while (task_list_.size() == 0) {
pthread_cond_wait(&cond_, &lock_);
}
// 獲取並執行任務,釋放任務資源
Task *task = task_list_.front();
task_list_.pop_front();
pthread_mutex_unlock(&lock_);
task->Run();
delete task;
}
}
} // namespace thread
我們知道,C++類的靜態函數沒有this指針,在靜態函數中只能調用靜態函數。StartWorker是一個靜態函數,而Do函數是一個非靜態函數,這里的技巧就是通過將參數argv傳遞一個this指針進來,然后通過C++的reinterpret_cast轉換成ThreadPool類型的指針,再通過這個指針調用Do函數。
最后,我們通過一個例子演示如何生成一個具體的任務Task,如何將Task添加到線程池,執行效果又是怎么樣的。
#include "thread_pool.h"
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <unistd.h>
// 簡單任務類,Run函數僅僅是打印字符串
class MyTask: public thread::Task {
public:
void Run();
void SetData(const std::string &data) {
data_ = data;
}
private:
std::string data_;
};
void MyTask::Run() {
printf("%s run over.\n", data_.c_str());
}
int main(int argc, char **argv) {
// 初始化線程池
thread::ThreadPool thread_pool;
thread_pool.Init(4);
char str[10] = "";
for (int i = 0; i < 10; ++i) {
// 初始化任務,僅僅是設置任務名稱
MyTask *task = new MyTask();
sprintf(str, "Task %d", i);
task->SetData(str);
// 添加任務到線程池
thread_pool.AddTask(task);
}
// 休眠100ms等待線程池任務執行完
usleep(100);
return 0;
}
編譯:g++ main.cpp thread_pool.cpp -lpthread
運行:./a.out
Task 0 run over.
Task 4 run over.
Task 5 run over.
Task 6 run over.
Task 7 run over.
Task 8 run over.
Task 9 run over.
Task 2 run over.
Task 3 run over.
Task 1 run over.
至此,一個白紙編程的線程池就完成了,它是一個很好的線程池原型,足以讓面試官產生“此人還是寫過幾行代碼的,我再出個5星級難度的算法題考考他的思考問題能力”的美妙想法……言歸正傳,工作中的線程池比這個簡單模型要功能強大很多,會對這個模型進行大量優化,以滿足工程需求。
那么,工業級別的線程池通常具備什么特點呢?可靠、穩定、高性能,這些高大上的詞都顯得太“虛”了,更為實際一點的答案是支持監控、靈活配置、自我調節能力和具有優雅退出功能。下面來淺談一下工作中線程池應具備的上述優良特點,以及這些特點實現的思路。
可監控:線程池最主要的監控指標只有一個,那就是任務堆積個數,通過這個指標我們可以直觀感受到線程池運行狀況。如果觀察到線程池任務堆積嚴重,這時候就要仔細分析原因,考慮是否需要調整線程池參數或者優化任務的處理邏輯了。在上述線程池原型中,線程池的任務堆積個數即task_queue_.size()。
支持靈活配置:是指線程池應提供足夠多的參數讓用戶去定制,例如最小線程個數、最大線程個數、任務超時時間等等。
自我調節能力:這個是線程池的高級功能,是指線程池中的worker線程個數可以由線程池自身動態調整,例如在任務很少的時候,主動減少worker線程數,例如可以將示例中ThreadPool::Do函數中的pthread_cond_wait改成pthread_cond_timewait,設置一個超時時間,如果達到超時時間則主動銷毀worker線程。很顯然還需要在任務數變多的時候主動增加worker線程個數,當然前提是不能超過線程池中的最大線程個數限制。這個特性可以通過修改AddTask接口來實現,每次添加任務時都判斷下當前任務隊列的任務數是否達到某個閾值,同時判斷worker線程數是否還能繼續增加。
優雅退出功能:程序在接收信號准備停止運行時,線程池中積壓的任務要處理完后才能退出程序,同時將資源有序釋放。
最后補充一點,就是代碼簡潔,好的線程池代碼一定是簡潔易懂、接口容易被正確使用的。以上就是我總結的后端系統中線程池的基本知識和相關技巧,希望能夠幫助朋友們掌握線程池的原理和使用,尤其在面試和長久的工作過程中有所幫助。
金句分享
年輕是一個中性詞,它代表着很多缺點:缺乏經驗、少不更事、容易沖動。但是也有很多優點,其中之一就是有大把的時間去遺忘那些不該記住的事情。
——出自《平凡的世界》,作者路遙,原名王衛國,中國著名作家。
解讀:年輕的時候要勤奮、謙虛,勇敢嘗試。