大多數線程池實現都離不開鎖的使用,如互斥量pthread_mutex*結合條件變量pthread_cond*。眾所周知,鎖的使用對于程序性能影響較大,雖然現有的pthread_mutex*在鎖的申請與釋放方面做了較大的優化,但是,線程池的實現是可以做到無鎖化的。
1.常見線程池實現原理
如上圖所示,工作隊列由主線程和工作者線程共享,主線程將任務放進工作隊列,工作者線程從工作隊列中取出任務執行。共享工作隊列的操作需在互斥量的保護下安全進行,主線程將任務放進工作隊列時若檢測到當前待執行的工作數目小于工作者線程總數,則需使用條件變量喚醒可能處于等待狀態的工作者線程。當然,還有其他地方可能也會使用到互斥量和條件變量,不再贅述。
2.無鎖化線程池實現原理
為解決無鎖化的問題,需要避免共享資源的競爭,因此將共享工作隊列加以拆分成每工作線程一個工作隊列的方式。對于主線程放入工作和工作線程取出任務的競爭問題,可以采取環形隊列的方式避免。在解決了鎖機制之后,就只剩下條件變量的問題了,條件變量本身即解決條件滿足時的線程通信問題,而信號作為一種通信方式,可以代替之,其大體編程范式為:
sigemptyset (&oldmask);
sigemptyset (&signal_mask);
sigaddset (&signal_mask, SIGUSR1);
rc = pthread_sigmask(SIG_BLOCK, &signal_mask, NULL);
if (rc != 0) {
debug(TPOOL_ERROR, "SIG_BLOCK failed");
return -1;
}
...
while (!condition) {
rc = sigwait (&signal_mask, NULL);
if (rc != 0) {
debug(TPOOL_ERROR, "sigwait failed");
return -1;
}
}
rc = pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
if (rc != 0) {
debug(TPOOL_ERROR, "SIG_SETMASK failed");
return -1;
}
需要C/C++ linux服務器架構師學習資料私信“資料”(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享
3.無鎖化線程池具體實現
在無鎖線程池中,區別于常見線程池的地方主要在于信號與條件變量、任務調度算法、增加或減少線程數目后的任務遷移,另外還有一點就是環形隊列的實現參考了Linux內核中的kfifo實現。
(1) 信號與條件變量
信號與條件變量的區別主要在于條件變量的喚醒(signal)對于接收線程而言可以忽略,而在未設置信號處理函數的情況下信號的接收會導致接收線程甚至整個程序的終止,因此需要在線程池產生線程之前指定信號處理函數,這樣新生的線程會繼承這個信號處理函數。多線程中信號的發送主要采用pthread_kill,為避免使用其他信號,本程序中使用了SIGUSR1。
(2) 任務調度算法
常見線程池實現的任務調度主要在操作系統一級通過線程調度實現。考慮到負載均衡,主線程放入任務時應采取合適的任務調度算法將任務放入對應的工作者線程隊列,本程序目前已實現Round-Robin和Least-Load算法。Round-Robin即輪詢式地分配工作,Least-Load即選擇當前具有最少工作的工作者線程放入。
(3) 任務遷移
在線程的動態增加和減少的過程中,同樣基于負載均衡的考量,涉及到現有任務的遷移問題。負載均衡算法主要基于平均工作量的思想,即統計當前時刻的總任務數目,均分至每一個線程,求出每個工作者線程應該增加或減少的工作數目,然后從頭至尾遍歷,需要移出工作的線程與需要移入工作的線程執行任務遷移,相互抵消。最后若還有多出來的工作,再依次分配。遷入工作不存在競態,因為加入工作始終由主線程完成,而遷出工作則存在競態,因為在遷出工作的同時工作者線程可能在同時執行任務。所以需要采用原子操作加以修正,其主要思想即預取技術,大致實現為:
do {
work = NULL;
if (thread_queue_len(thread) <= 0) //also atomic
break;
tmp = thread->out;
//prefetch work
work = &thread->work_queue[queue_offset(tmp)];
} while (!__sync_bool_compare_and_swap(&thread->out, tmp, tmp + 1));
if (work) {
// do something在線程的動態減少后,原先線程上未能執行完的任務只需要由
//主線程再次根據任務調度算法重新分配至其他存活的工作者線程隊列中即可,不
//存在上述問題,當然,此時可以同時執行負載均衡算法加以優化。
}
(4) 環形隊列
源碼中環形隊列實現主要參考了linux內核中kfifo的實現,如下圖所示:
隊列長度為2的整次冪,out和in下標一直遞增至越界后回轉,其類型為unsigned int,即out指針一直追趕in指針,out和in映射至FiFo的對應下標處,其間的元素即為隊列元素。