本篇目標
前一篇(學習執行緒池 1:設計方陣乘法的API)重點在於定義API,這篇的重點在於「如何設計 struct tpool」,目標是定義執行緒池的基本結構。
最終程式碼:Cpp-Projects/thread-pool/thread-pool.c at main · loijilai/Cpp-Projects (github.com)
我們要解決的問題
- 如何傳遞計算任務?
- 需要一個task queue來讓主執行緒存放方陣計算請求
- 如何管理眾多執行緒?
- 需要一個固定大小的執行緒陣列
task queue設計
我們先處理第一個問題:如何傳遞計算任務?
首先我們會需要一個task structure存放切分過後的request。
typedef struct task {
int row_index;
Matrix A;
Matrix B;
Matrix C;
struct task *next;
} task_t;這表示每個工作執行緒會計算A * B = C中,C的第row_index個row的值。
這部分在討論用鏈結串列實作佇列,如果對DSA很熟的讀者可以放心跳過。
在用C實作queue上通常有分兩種選擇:
- 初始化時,head tail都指向NULL,操作過程全程保持head指向佇列中第一個元素,tail指向佇列中最後一個元素
- 初始化時,head tail都指向dummy,操作全程保持head指向dummy node,tail指向佇列中最後一個元素
第一個方法要注意
// 初始化
q->head = NULL;
q->tail = NULL;- 當enqueue是第一個元素,也就是tail == NULL,要同步更新head
這讓人覺得有點煩,因為enqueue應該把重點放在tail的更新就好,很容易忘記同步更新head``` - 當dequeue的是最後一個元素,也就是head == NULL,要同步更新tail
同理,dequeue的時候應該要把重點放在head的更新就好,很容易忘記同步更新tail
第二個方法讓head永遠指向dummy node
// 初始化
struct QNode *dummy = malloc(sizeof(QNode));
q->head = dummy;
q->tail = dummy;為了讓enqueue專注在tail,dequeue專注在head,我們採用第二個方法:
- 初始化時讓head和tail都指向dummy node。
- 整個enqueue/dequeue的過程中,head永遠指向dummy node,而tail指向最後一個元素,若queue中沒有元素則tail也指向dummy node。
這樣的設計可以簡化enqueue和dequeue的邏輯。
tpool structure設計
了解task queue的設計之後,我們要開始設計tpool_t。
剛剛第一個問題是如何傳遞計算任務?所以設計一個task queue來讓主執行緒存放方陣計算請求
task_t *task_queue_head;
task_t *task_queue_tail;
pthread_mutex_t task_queue_mutex;
pthread_cond_t task_queue_cond;但因為主執行緒放任務,和工作執行緒取任務之間都會存取task queue,另外,工作執行緒之間也都會存去task queue,針對這種共享資源我們要新增一個task_queue_mutex。
另外我們不希望當task queue為空,工作執行緒一直busy waiting,所以設定一個condition variable task_queue_cond,用來讓主執行緒通知工作執行緒現在有task可以取用。
本篇的第二個問題是如何管理眾多執行緒?這部分我們需要一個固定大小的執行緒陣列。
int num_threads;
pthread_t *threads;整體的tpool設計看起來會是這樣,至於其他的狀態管理變數pending_tasks, stop和sync_cond則會在後續提到,目前可以先忽略。
typedef struct tpool {
// 執行緒相關
int num_threads; // 執行緒數量
pthread_t *threads; // 執行緒陣列
// 工作佇列相關
task_t *task_queue_head; // 佇列頭(dummy node)
task_t *task_queue_tail; // 佇列尾
pthread_mutex_t task_queue_mutex; // 佇列鎖
pthread_cond_t task_queue_cond; // 佇列非空條件變數
// 其他read-only共享變數
int matrix_size; // 矩陣大小(n * n)
// 以下可以先忽略
// 追蹤計算狀態
int pending_tasks; // 當前正在處理的任務數量
pthread_cond_t sync_cond; // 讓tpool_synchronize()等待所有任務完成
// 執行緒池關閉管理
bool stop; // 控制執行緒何時結束
} tpool_t;
下一步
下一篇學習執行緒池 3:如何讓主執行緒提交計算請求?會涵蓋:
tpool_request()如何將新工作加入佇列- 執行緒如何從佇列取出並執行 → 生產者-消費者問題
- 佇列需要執行緒安全,因為主執行緒會不斷加入任務,而執行緒池內的工作執行緒會從佇列取出並執行