上一篇博客我們實現了讀者寫者模型,在這篇博客中更進一步的完成線程池。
線程池概念
開闢一塊內存空間,裏面存在大量的(未死亡的)線程,池中的線程調度由池管理器來處理,當有線程任務時,從池中選一個線程運行,當運行完畢後,該線程又返回池中,這樣就避免了反覆創建線程所帶來的性能開銷,節省了系統資源。
如果對線程概念不清晰的話,不妨先看看我是一個線程這篇文章
線程池的應用場景
- 需要大量的線程來完成任務,且完成任務的時間比較短,例如WEB服務器完成網頁請求。
- 對性能要求苛刻的應用,比如要求服務器立即響應客戶請求
- 接受突發性的大量請求,但是不至於時服務器因此產生大量的線程的應用。
線程池就是圍繞上面幾種情況而產生的,下面我們自己實現一個線程池,暫時無法體現出線程池的應用場景。
線程池的實現
我們需要設計線程池這個類的成員有哪些。
- 計數器 用於記錄空餘線程的個數
- 任務隊列:用於儲存任務的緩衝區
- 信號量
- 鎖
class thread_pool
{
private:
size_t thread_num;//記錄當前有多少進程空閒
queue<Task> thread_queue;
pthread_cond_t cond;
pthread_mutex_t mutex;
}
構造函數和析構函數主要是對信號量和鎖進行初始化
thread_pool(int num = 5): thread_num(num)
{
pthread_cond_init(&cond,nullptr);
pthread_mutex_init(&mutex, nullptr);
}
~thread_pool()
{
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
}
創建線程,對線程池初始化
void Init_thread_pool()
{
pthread_t id;
for(int i = 0; i < thread_num; i++)
{
pthread_create(&id, nullptr, Run_func, this);//注意把this作爲變量傳給Run_func,這裏只做演示只寫了一個運行的函數。
}
}
下面是整個線程池的重點,執行任務。
static void* Run_func(void* arg)
{
pthread_detach(pthread_self());
thread_pool* pool = (thread_pool*) arg;
while (1)
{
pool->thread_queue;
while (pool->Is_empty())
{
pool->thread_Idle();//idle
}
Task it;
pool->pop_task(it);
pool->unlock_queue();
it.Run();
}
首先,因爲pthread_create函數的第三個參數爲 void *(*start_routine) (void *),但是對於類中的非靜態函數,已經隱含的傳入了this變量,因此我們必須把該函數設爲靜態變量,並且把this指針作爲參數傳入函數中。
其次,可以發現使用while循環而不是用if判斷隊列是否爲空,這是因爲有多個線程可能會同時操作,while循環能夠防止誤判,下面這一段雖然說的是生產者消費者模式的wait,但是我覺得也能說明問題
永遠不要在循環之外調用wait方法
《Effective Java》第二版中文版第69條244頁位置對這一點說了一頁,生產者和消費者問題來說:錯誤情況一:如果有兩個生產者A和B,一個消費者C。當存儲空間滿了之後,生產者A和B都被wait,進入等待喚醒隊列。當消費者C取走了一個數據後,如果調用了notifyAll(),注意,此處是調用notifyAll(),則生產者線程A和B都將被喚醒,如果此時A和B中的wait不在while循環中而是在if中,則A和B就不會再次判斷是否符合執行條件,都將直接執行wait()之後的程序,那麼如果A放入了一個數據至存儲空間,則此時存儲空間已經滿了;但是B還是會繼續往存儲空間裏放數據,錯誤便產生了。錯誤情況二:如果有兩個生產者A和B,一個消費者C。當存儲空間滿了之後,生產者A和B都被wait,進入等待喚醒隊列。當消費者C取走了一個數據後,如果調用了notify(),則A和B中的一個將被喚醒,假設A被喚醒,則A向存儲空間放入了一個數據,至此空間就滿了。A執行了notify()之後,如果喚醒了B,那麼B不會再次判斷是否符合執行條件,將直接執行wait()之後的程序,這樣就導致向已經滿了數據存儲區中再次放入數據。錯誤產生。
最後我們發現,任務的執行是在鎖外面而不是在鎖裏面,難道不管進程安全了嗎?,仔細想想,發現當得到了分配的線程就沒人和你競爭了,既然沒人和你競爭了,自然也不需要鎖的保護了,另外假如我們讓任務的執行放到鎖裏面,事實上,是讓程序變成了串行,效率還不如之前。
完成只差一步,下面的函數,用來傳入任務,喚醒函數是爲了防止隊列裏原來沒有任務,插入新的任務需要喚醒。
void push_task(const Task& t)
{
lock_queue();
thread_queue.push(t);
wake_thread();
unlock_queue();
}
完成了上面的函數,基本上就把線程池完成了大半,下面是完整的代碼。
#include <queue>
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
using namespace std;
int cal(int x, int y, int op);
typedef int (*HandlerTask_t)(int x,int y,int op);//定義一個函數指針
class Task
{
private:
int _left;
int _right;
int _op;
HandlerTask_t _handler;
public:
Task(int left = 0, int right = 0, int op = 0) :_left(left), _right(right), _op(op)
{
}
void Register(HandlerTask_t handler)
{
_handler = handler;
}
void Run()//任務執行
{
int ret = _handler(_left, _right, _op);
const char* str = "+-*/";
cout << "thread is [" << pthread_self() << _left << str[_op] << _right << "=" << ret << endl;
}
~Task()
{
}
};
class thread_pool
{
private:
size_t thread_num;//記錄當前有多少進程空閒
queue<Task> thread_queue;
pthread_cond_t cond;
pthread_mutex_t mutex;
public:
thread_pool(int num = 5): thread_num(num)
{
pthread_cond_init(&cond,nullptr);
pthread_mutex_init(&mutex, nullptr);
}
~thread_pool()
{
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
}
void Init_thread_pool()
{
pthread_t id;
for(int i = 0; i < thread_num; i++)
{
pthread_create(&id, nullptr, Run_func, this);//注意把this作爲變量傳給Run_func,這裏只做演示只寫了一個運行的函數。
}
}
void lock_queue()
{
pthread_mutex_lock(&mutex);
}
void unlock_queue()
{
pthread_mutex_unlock(&mutex);
}
bool Is_empty()
{
return thread_queue.size() == 0;
}
void thread_Idle()
{
thread_num++;
pthread_cond_wait(&cond, &mutex);
thread_num--;
}
void wake_thread()
{
pthread_cond_signal(&cond);
}
void pop_task(Task& t)
{
t = thread_queue.front();
thread_queue.pop();
}
void push_task(const Task& t)
{
lock_queue();
thread_queue.push(t);
wake_thread();
unlock_queue();
}
static void* Run_func(void* arg)
{
pthread_detach(pthread_self());
thread_pool* pool = (thread_pool*) arg;
while (1)
{
pool->thread_queue;
while (pool->Is_empty())
{
pool->thread_Idle();//idle
}
Task it;
pool->pop_task(it);
pool->unlock_queue();
it.Run();
}
}
};
int cal(int x, int y, int op)//模擬一個計算器
{
int ret = -1;
switch (op)
{
case 0:
ret = x + y;
break;
case 1:
ret = x - y;
break;
case 2:
ret = x * y;
break;
case 3:
ret = x / y;
break;
default:
cout << "cal error!" << endl;
break;
}
}
int main()
{
thread_pool tp;
tp.Init_thread_pool();
srand((unsigned long)time(NULL));
for (;;) {
int x = rand() % 100 + 1;
int y = rand() % 100 + 1;
int op = rand() % 4;
Task t(x, y, op);
t.Register(cal);
tp.push_task(t);
sleep(1);
}
return 0;
}
運行結果如下(仔細想想是真的閒)
總結
線程池是爲了需要大量的線程來完成任務,且完成任務的時間比較短的情況而產生的,節省了開闢銷燬線程的資源開銷,可以使用多種方式實現.
完成了上面的代碼,我們對於linux的線程的學習告一段落,在下篇博客中我們終於可以學習網絡相關的知識。