執行緒池的實現程式碼分析

惠hiuj發表於2024-06-09

[toc]

執行緒池

執行緒池程式碼分析

image

thread_pool.c

#include "thread_pool.h"

void handler(void *arg)
{
	printf("[%u] is ended.\n",
		(unsigned)pthread_self());										//列印自己的程序號

	pthread_mutex_unlock((pthread_mutex_t *)arg);						//解鎖
}

//執行緒要執行的任務
void *routine(void *arg)
{
	//除錯
	#ifdef DEBUG
	printf("[%u] is started.\n",
		(unsigned)pthread_self());
	#endif

	//把需要傳遞給執行緒任務的引數進行備份
	thread_pool *pool = (thread_pool *)arg;								
	struct task *p;

	while(1)
	{
		/*
		** push a cleanup functon handler(), make sure that
		** the calling thread will release the mutex properly
		** even if it is cancelled during holding the mutex.
		**
		** NOTE:
		** pthread_cleanup_push() is a macro which includes a
		** loop in it, so if the specified field of codes that 
		** paired within pthread_cleanup_push() and pthread_
		** cleanup_pop() use 'break' may NOT break out of the
		** truely loop but break out of these two macros.
		** see line 61 below.
		*/
		//================================================//
		pthread_cleanup_push(handler, (void *)&pool->lock);   //註冊清理執行緒函式
		pthread_mutex_lock(&pool->lock);					  //上鎖
		//================================================//

		// 1, no task, and is NOT shutting down, then wait
		while(pool->waiting_tasks == 0 && !pool->shutdown){   //判斷處於等待狀態的執行緒是否等於0,以及判斷是否需要銷燬執行緒池,這裡是如果false,則進入迴圈
			pthread_cond_wait(&pool->cond, &pool->lock);     //會將呼叫執行緒放入條件變數的等待佇列,並釋放互斥鎖。執行緒在被喚醒之前會一直阻塞。當執行緒被喚醒後,它會重新獲取互斥鎖,然後繼續執行
		}

		// 2, no task, and is shutting down, then exit
		if(pool->waiting_tasks == 0 && pool->shutdown == true) //如果需要銷燬,則解鎖然後執行緒退出
		{
			pthread_mutex_unlock(&pool->lock);
			pthread_exit(NULL); // CANNOT use 'break';
		}

		// 3, have some task, then consume it   //連結串列的頭刪,表示當有任務連結串列中有任務時,處於等待佇列的執行緒就會去,執行任務連結串列中的表頭的任務
		p = pool->task_list->next;				//將連結串列的首結點地址,給到區域性變數的P
		pool->task_list->next = p->next;		//讓連結串列的頭結點,指向P->next ,也就是首結點的下一個結點
		pool->waiting_tasks--;					//讓等待被執行緒執行的任務數量減1

		//================================================//
		pthread_mutex_unlock(&pool->lock);						//解鎖
		pthread_cleanup_pop(0);									//不取消清理函式,也不執行
		//================================================//

		pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);   
		(p->do_task)(p->arg);									//執行連結串列中的函式
		pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);	

		free(p);												//執行完任務結點後釋放區域性變數下儲存的堆記憶體地址
	}

	pthread_exit(NULL);
}

//初始化執行緒池
bool init_pool(thread_pool *pool, unsigned int threads_number)
{
	//初始化互斥鎖
	pthread_mutex_init(&pool->lock, NULL);

	//初始化條件量
	pthread_cond_init(&pool->cond, NULL);

	//銷燬標誌 
	pool->shutdown = false; //不銷燬

	//給連結串列的節點申請堆記憶體
	pool->task_list = malloc(sizeof(struct task)); 

	//申請堆記憶體,用於儲存建立出來的執行緒的ID
	pool->tids = malloc(sizeof(pthread_t) * MAX_ACTIVE_THREADS);      //執行緒池中最大執行緒數量= 活躍執行緒+能夠建立的執行緒數量(既能夠存放執行緒ID的個數)  

	//錯誤處理,對malloc進行錯誤處理
	if(pool->task_list == NULL || pool->tids == NULL)
	{
		perror("allocate memory error");
		return false;
	}

	//對任務連結串列中的節點的指標域進行初始化
	pool->task_list->next = NULL;

	//設定執行緒池中執行緒數量的最大值
	pool->max_waiting_tasks = MAX_WAITING_TASKS;			//方便更改
	
	//設定等待執行緒處理的任務的數量為0,說明現在沒有任務
	pool->waiting_tasks = 0;

	//設定執行緒池中活躍的執行緒的數量
	pool->active_threads = threads_number;

	int i;

	//迴圈建立活躍執行緒
	for(i=0; i<pool->active_threads; i++)                   
	{
		//建立執行緒  把執行緒的ID儲存在申請的堆記憶體
		if(pthread_create(&((pool->tids)[i]), NULL,					
					routine, (void *)pool) != 0)
		{
			perror("create threads error");
			return false;
		}

		//用於除錯
		#ifdef DEBUG
		printf("[%u]:[%s] ==> tids[%d]: [%u] is created.\n",
			(unsigned)pthread_self(), __FUNCTION__,
			i, (unsigned)pool->tids[i]);
		#endif
	}

	return true;
}

//先執行緒池的任務連結串列中新增任務
bool add_task(thread_pool *pool,
	      void *(*do_task)(void *arg), void *arg)           //將自定義的任務,新增到
{
	//給任務連結串列節點申請記憶體
	struct task *new_task = malloc(sizeof(struct task));
	if(new_task == NULL)
	{
		perror("allocate memory error");
		return false;
	}

	new_task->do_task = do_task;	//設定需要在連結串列中新增的任務
	new_task->arg = arg;			//任務函式的引數
	new_task->next = NULL;			//指標域設定為NULL

	//============ LOCK =============//
	pthread_mutex_lock(&pool->lock);			   //上鎖防止主執行緒與任務執行執行緒,資源競爭
	//===============================//

	//說明要處理的任務的數量大於能處理的任務數量
	if(pool->waiting_tasks >= MAX_WAITING_TASKS)   
	{
		pthread_mutex_unlock(&pool->lock);		  

		fprintf(stderr, "too many tasks.\n");
		free(new_task);							 

		return false;
	}
	
	struct task *tmp = pool->task_list;		 //透過區域性變數,對任務連結串列進行訪問,可以保留頭結點的位置,減少持有鎖的時間,隨著函式結束自動釋放資源

	//遍歷連結串列,找到單向連結串列的尾節點
	while(tmp->next != NULL)
		tmp = tmp->next;

	//把新的要處理的任務插入到連結串列的尾部  尾插
	tmp->next = new_task;

	//要處理的任務的數量+1
	pool->waiting_tasks++;

	//=========== UNLOCK ============//
	pthread_mutex_unlock(&pool->lock);
	//===============================//

	//除錯
	#ifdef DEBUG
	printf("[%u][%s] ==> a new task has been added.\n",
		(unsigned)pthread_self(), __FUNCTION__);
	#endif

	//喚醒第一個處於阻塞佇列中的執行緒
	pthread_cond_signal(&pool->cond);
	return true;
}

//向執行緒池加入新執行緒
int add_thread(thread_pool *pool, unsigned additional_threads)
{
	//判斷需要新增的新執行緒的數量是否為0
	if(additional_threads == 0)
		return 0;

	//計算執行緒池中匯流排程的數量
	unsigned total_threads =
			pool->active_threads + additional_threads;   //活躍執行緒的數量+需要新增新執行緒的數量=總的活躍執行緒的數量

	int i, actual_increment = 0;       					 

	
	for(i = pool->active_threads;i < total_threads && i < MAX_ACTIVE_THREADS;i++) //判斷條件為 活躍執行緒的數量小於執行緒池中匯流排程的數量 且 小於建立執行緒的上限
	{																				//執行緒總的活躍執行緒的數量,是用來判斷迴圈多少次的
		//建立新執行緒
		if(pthread_create(&((pool->tids)[i]),										//而超過能夠建立的執行緒數量也一樣不會在新增新執行緒了
					NULL, routine, (void *)pool) != 0)								//為什麼不這樣寫for(i=0 , i<additional_threads, && i < MAX_ACTIVE_THREADS;i++) 
		{																			//因為建立出來的執行緒ID,會根據建立時的順序,存放到對應的陣列下標的位置,
			perror("add threads error");											//所以需要定義兩個區域性變數,用於儲存陣列最後的一個元素的後一個位置,因為是i++  ,int i, actual_increment = 0;
																					//actual_increment這個變數用於儲存實際建立執行緒的數量
			// no threads has been created, return fail
			if(actual_increment == 0)												
				return -1;

			break;
		}
		actual_increment++; 

		#ifdef DEBUG
		printf("[%u]:[%s] ==> tids[%d]: [%u] is created.\n",
			(unsigned)pthread_self(), __FUNCTION__,
			i, (unsigned)pool->tids[i]);
		#endif
	}

	//記錄此時執行緒池中活躍執行緒的總數
	pool->active_threads += actual_increment;
	return actual_increment;
}

int remove_thread(thread_pool *pool, unsigned int removing_threads)				//刪除執行緒池中的執行緒。 引數是需要刪除的數量
{
	if(removing_threads == 0)
		return pool->active_threads;											

	int remaining_threads = pool->active_threads - removing_threads;			//計算執行緒池中剩餘的執行緒數量			
	remaining_threads = remaining_threads > 0 ? remaining_threads : 1;          //防呆,如果想要刪除的執行緒數量大於執行緒池中執行緒的數量則 
																				//保留初始化時陣列下標為0的執行緒,如果不保留,就是銷燬執行緒池了
	int i;																		//在刪除執行緒池中的執行緒時,保留至少一個執行緒的原因是為了確保執行緒池仍然能夠處理剩餘的任務,避免完全停止服務。
	for(i=pool->active_threads-1; i>remaining_threads-1; i--)					//pool->active_threads-1即陣列中最後一個元素下標
	{
		errno = pthread_cancel(pool->tids[i]);									// 取消執行緒 同時 執行執行緒清理函式,主執行緒呼叫,終止其他執行緒

		if(errno != 0)
			break;

		#ifdef DEBUG
		printf("[%u]:[%s] ==> cancelling tids[%d]: [%u]...\n",
			(unsigned)pthread_self(), __FUNCTION__,
			i, (unsigned)pool->tids[i]);
		#endif
	}

	if(i == pool->active_threads-1)												
		return -1;
	else
	{
		pool->active_threads = i+1;
		return i+1;
	}
}

bool destroy_pool(thread_pool *pool)								//銷燬執行緒池
{
	// 1, activate all threads
	pool->shutdown = true;
	pthread_cond_broadcast(&pool->cond);							//喚醒執行緒池中所有執行緒,然後終止執行緒

	// 2, wait for their exiting
	int i;
	for(i=0; i<pool->active_threads; i++)							
	{
		errno = pthread_join(pool->tids[i], NULL);                //主執行緒呼叫,迴圈等待回收執行緒資源
		if(errno != 0)
		{
			printf("join tids[%d] error: %s\n",
					i, strerror(errno));
		}
		else
			printf("[%u] is joined\n", (unsigned)pool->tids[i]);
		
	}

	// 3, free memories
	free(pool->task_list);											//釋放執行緒池中任務連結串列的堆記憶體
	free(pool->tids);												//釋放執行緒控制代碼陣列的堆記憶體
	free(pool);														//釋放管理執行緒池的堆記憶體

	return true;
}

thread_pool.h

#ifndef _THREAD_POOL_H_
#define _THREAD_POOL_H_

#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

#include <errno.h>
#include <pthread.h>

#define MAX_WAITING_TASKS	1000   //處於等待狀態的執行緒數量最大為1000
#define MAX_ACTIVE_THREADS	20     //活躍的執行緒數量 

//任務結點  單向連結串列的節點,型別
struct task
{
	void *(*do_task)(void *arg); //任務函式指標  指向執行緒要執行的任務  格式是固定的
	void *arg;					 //需要傳遞給任務的引數,如果不需要,則NULL

	struct task *next;			 //指向下一個任務結點的指標
};

//執行緒池的管理結構體
typedef struct thread_pool
{
	pthread_mutex_t lock;		// 互斥鎖
	pthread_cond_t  cond;		// 條件量

	bool shutdown;				//是否需要銷燬執行緒池

	struct task *task_list;		//用於儲存任務的連結串列

	pthread_t *tids;			//用於記錄執行緒池中執行緒的ID,其實是一個陣列,陣列中元素的型別是pthread_t

	unsigned max_waiting_tasks;	//執行緒池中執行緒的數量最大值
	unsigned waiting_tasks;		//設定等待執行緒處理的任務的數量
	unsigned active_threads;	//正在活躍的執行緒數量
}thread_pool;

//初始化執行緒池
bool init_pool(thread_pool *pool, unsigned int threads_number);

//向執行緒池中新增任務
bool add_task(thread_pool *pool, void *(*do_task)(void *arg), void *task);

//先執行緒池中新增執行緒
int  add_thread(thread_pool *pool, unsigned int additional_threads_number);

//從執行緒池中刪除執行緒
int  remove_thread(thread_pool *pool, unsigned int removing_threads_number);

//銷燬執行緒池
bool destroy_pool(thread_pool *pool);

//任務函式
void *routine(void *arg);

#endif

main.c

#include "thread_pool.h"

// 任務函式:模擬一個需要耗時的任務
void *mytask(void *arg)
{
    int n = (int)arg;

    printf("[%u][%s] ==> job will be done in %d sec...\n",
           (unsigned)pthread_self(), __FUNCTION__, n);

    sleep(n); // 模擬任務耗時

    printf("[%u][%s] ==> job done!\n",
           (unsigned)pthread_self(), __FUNCTION__);

    return NULL;
}

// 計時函式:每秒列印一次經過的秒數
void *count_time(void *arg)
{
    int i = 0;
    while(1)
    {
        sleep(1);
        printf("sec: %d\n", ++i);
    }
}

int main(void)
{
    pthread_t a;
    // 建立計時執行緒
    pthread_create(&a, NULL, count_time, NULL);

    // 1. 初始化執行緒池
    thread_pool *pool = malloc(sizeof(thread_pool));
    init_pool(pool, 2); // 初始化執行緒池,初始執行緒數為2

    // 2. 新增任務到執行緒池
    printf("throwing 3 tasks...\n");
    add_task(pool, mytask, (void *)(rand()%10));
    add_task(pool, mytask, (void *)(rand()%10));
    add_task(pool, mytask, (void *)(rand()%10));

    // 3. 檢查當前活動執行緒數
    printf("current thread number: %d\n",
           remove_thread(pool, 0)); // 傳入0表示不移除執行緒,只返回當前執行緒數
    sleep(9); // 等待9秒

    // 4. 新增更多工到執行緒池
    printf("throwing another 2 tasks...\n");
    add_task(pool, mytask, (void *)(rand()%10));
    add_task(pool, mytask, (void *)(rand()%10));

    // 5. 新增更多執行緒到執行緒池
    add_thread(pool, 2); // 增加2個執行緒

    sleep(5); // 等待5秒

    // 6. 從執行緒池中移除執行緒
    printf("remove 3 threads from the pool, "
           "current thread number: %d\n",
           remove_thread(pool, 3)); // 移除3個執行緒,並列印當前執行緒數

    // 7. 銷燬執行緒池
    destroy_pool(pool); // 銷燬執行緒池
    return 0; // 主函式返回0,程式結束
}

image
image
image
image

執行緒取消的基本概念

  • 取消點(Cancellation Point):執行緒在執行過程中,會在一些特定的函式呼叫時檢查是否有取消請求,這些函式稱為取消點。例如,pthread_testcancelpthread_joinpthread_cond_wait 等函式都是取消點。

  • 取消型別(Cancellation Type):決定執行緒在取消點如何響應取消請求。主要有兩種型別:

    • 非同步取消(Asynchronous Cancellation):執行緒可以在任何時刻被取消。
    • 延遲取消(Deferred Cancellation):執行緒只有在到達取消點時才會被取消。POSIX 執行緒庫預設使用這種型別。
  • 取消狀態(Cancellation State):決定執行緒是否響應取消請求。可以是以下兩種狀態:

    • 啟用(Enable):執行緒會響應取消請求。
    • 禁用(Disable):執行緒不會響應取消請求。

    pthread_cleanup_push 是 POSIX 執行緒庫(pthread)中的一個函式,用於線上程取消時執行清理操作。它與 pthread_cleanup_pop 配對使用,確保線上程退出或被取消時執行特定的清理程式碼,例如釋放資源或解鎖互斥鎖。

    • pthread_cleanup_push(cleanup, "Resource A"):註冊清理函式 cleanup,當執行緒被取消或退出時,會執行 cleanup("Resource A")
    • pthread_cleanup_pop(1):取消清理函式並執行它(引數 1 表示執行清理函式)。
    • pthread_testcancel():這是一個取消點,執行緒在這裡檢查是否有取消請求。
      pthread_cleanup_pushpthread_cleanup_pop 用於確保執行緒在被取消或正常退出時執行特定的清理操作。這對於管理資源(如記憶體、檔案描述符、互斥鎖等)非常重要,確保不會因為執行緒的意外終止而導致資源洩漏。

    執行緒取消(Thread Cancellation)是指在多執行緒程式設計中,允許一個執行緒請求終止另一個執行緒的執行。POSIX 執行緒庫提供了這種機制,使得執行緒可以被其他執行緒取消。這在某些情況下非常有用,例如當一個執行緒因為某種原因需要提前終止另一個執行緒的執行時。

相關函式

  • pthread_cancel(pthread_t thread): 請求取消指定的執行緒。
  • pthread_setcancelstate(int state, int *oldstate): 設定執行緒的取消狀態。
  • pthread_setcanceltype(int type, int *oldtype): 設定執行緒的取消型別。
  • pthread_testcancel(): 建立一個取消點,執行緒在執行到這裡時會檢查是否有取消請求。

透過執行緒池去管理執行緒:重點在於用條件變數,以及線性表(陣列)儲存執行緒的ID(控制代碼)
透過連結串列去管理任務: 尾插,頭刪的方式

閱讀程式碼的時候需要結合上下文
布林型別一般常用來判斷是和否(即二進位制可作為標誌)
函式指標:用來指向函式的指標 void *(*do_task)(void *arg);

釋放堆記憶體時,包括結構體裡的指標的堆記憶體分配

整體的架構應該是:

堵塞佇列:執行緒1 , 執行緒2 ,執行緒3 .......
任務連結串列: 任務1 , 任務2 ,任務3 .........

pthread_執行緒1{
上鎖
while{
條件量
}
if{判斷標誌位
銷燬執行緒池
}
pool->taks_list->next = p->next(頭刪)
(p->task_任務1)(p->arg)
解鎖
free(p)
}

相關文章