Linux:帶你理解線程池


概念

線程的池子,有很多線程,但是數量不會超過池子的限制。(需要用到多執行流並行進行任務處理的時候,就從池子中取出)

一種線程使用模式。線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。而線程池維護着多個線程,等待着監督管理者分配可併發執行的任務。這避免了在處理短時間任務時創建與銷燬線程的代價。線程池不僅能夠保證內核的充分利用,還能防止過分調度。可用線程數量應該取決於可用的併發處理器、處理器內核、內存、網絡 sockets等的數量。

爲什麼用線程池

有大量的數據處理請求,需要多執行流併發/並行處理

若是一個數據請求的到來伴隨一個線程的創建去處理,則會產生一些風險以及一些不必要的消耗:

  1. 線程若不限制數量的創建,在峯值壓力下,線程創建過多,資源耗盡,有程序崩潰的風險
  2. 處理一個任務的時間:創建線程時間t1+任務處理時間t2+線程銷燬時間t3 = T。若t2/T比例佔據不夠高,則表示大量的資源用於線程的創建與銷燬成本上,因此線程池使用已經創建好的線程進行循環任務處理,就避免了大量線程的頻繁創建於銷燬的時間成本.

應用場景

  1. 需要大量的線程來完成任務,且完成任務的時間比較短。 WEB服務器完成網頁請求這樣的任務,使用線程池技術是非常合適的。因爲單個任務小,而任務數量巨大,你可以想象一個熱門網站的點擊次數。 但對於長時間的任務,比如一個Telnet連接請求,線程池的優點就不明顯了。因爲Telnet會話時間比線程的創建時間大多了。

  2. 對性能要求苛刻的應用,比如要求服務器迅速響應客戶請求。

  3. 接受突發性的大量請求,但不至於使服務器因此產生大量線程的應用。突發性大量客戶請求,在沒有線程池情況下,將產生大量線程,雖然理論上大部分操作系統線程數目最大值不是問題,短時間內產生大量線程可能使內存到達極限,出現錯誤.

實現一個線程池

大量線程(每個線程中都是進行循環的任務處理)+ 任務緩衝隊列

  • 線程的入口函數,都是在創建線程的時候,就固定傳入的,導致線程池中的線程進行任務處理的方式過於單一
  • 因爲線程的入口函數都是一樣的,處理流程也就都是一樣的,只能處理單一方式的請求。 - 靈活性太差

若任務隊列中的任務,不僅僅是單純的數據,而是包含任務處理方式在內的數據,這時候,線程池中的線程只需要使用傳入的方式,處理傳入的數據即可,不需要關心是什麼數據,如何處理。 - 提高線程池的靈活性

typedef void (*_handler)(int data);
class MyTask{
private:
    int _data;    // 要處理的數據
    handle_t handler;    // 處理數據的方法
public:
    SetTask(int data, handler_t handler);    // 讓用戶自己傳入要處理的數據和方法,組織出一個任務節點
    Run(){ 
        return _handle(_data);
    }
};

每個線程的入口函數中,只需要不斷的獲取任務節點,調用任務節點中的Run接口就可以實現任務的處理。

#include <iostream>
#include <cstdio>
#include <queue>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define MAX_THREAD 5

typedef void (*handler_t)(int);

class ThreadTask{
public:
	ThreadTask():_data(-1), _handler(NULL)
	{
	}
	void SetTask(int data, handler_t handler){
		_data = data;
		_handler = handler;
	}
	void Run(){	// 外部只需要調用Run,不需要關心任務如何處理
		return _handler(_data);
	}
private:
	int _data;
	handler_t _handler;
};

class ThreadPool{
public:	
	ThreadPool(int max_thr = MAX_THREAD):
		_thr_max(max_thr)
	{
		pthread_mutex_init(&_mutex, NULL);
		pthread_cond_init(&_cond, NULL);
		for(int i = 0; i < _thr_max; i++){
			pthread_t tid;
			int ret = pthread_create(&tid, NULL, thr_start, this);
			if(ret != 0){
				printf("thread create error\n");
				exit(-1);
			}
		}
	}
	~ThreadPool(){
		pthread_mutex_destroy(&_mutex);
		pthread_cond_destroy(&_cond);
	}
	bool TaskPush(ThreadTask &task){
		pthread_mutex_lock(&_mutex);
		_queue.push(task);
		pthread_mutex_unlock(&_mutex);
		pthread_cond_broadcast(&_cond);	// 入隊後喚醒所有線程,誰搶到誰處理
		return true;
	}

	// 類的成員函數,有一個隱藏的默認參數(this指針)
	// 線程的入口函數只有一個參數,所以設置爲static
	static void *thr_start(void * arg){
		ThreadPool *p = (ThreadPool *) arg;
		// 不斷的從任務隊列中取出任務,執行任務的Run接口就可以
		while(1){
			pthread_mutex_lock(&p->_mutex);
			while(p->_queue.empty()){
				pthread_cond_wait(&p->_cond, &p->_mutex);
			}
			ThreadTask task;
			task = p->_queue.front();
			p->_queue.pop();
			pthread_mutex_unlock(&p->_mutex);
			task.Run();	// 任務的處理要放在解鎖之外,因爲當前的鎖保護的是隊列的操作
		}
		return NULL;
	}
private:
	int _thr_max;	// 線程池中線程的最大數量 - 根據這個初始化創建指定數量的線程
	std::queue<ThreadTask> _queue;
	pthread_mutex_t _mutex;	// 保護隊列操作的互斥鎖
	pthread_cond_t _cond;	// 實現從隊列中獲取節點的同步條件變量
};

void test_func(int data){
	int sec = (data % 3) + 1;
	printf("tid:%p -- get data:%d , sleep:%d\n", pthread_self(), data, sec);
	sleep(sec);
}

void tmp_func(int data){
	printf("tif:%p -- tmp_func\n", pthread_self());
	sleep(1);
}

int main(){
	ThreadPool pool;
	for(int i = 0; i < 10; i++){
		ThreadTask task;
		if(i % 2 == 0)
			task.SetTask(i, test_func);
		else
			task.SetTask(i, tmp_func);

		pool.TaskPush(task);
	}
	sleep(1000);
	return 0;
}

一次運行結果:

在這裏插入圖片描述


如果本篇博文有幫助到您,留個贊激勵一下博主吶~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章