大多數(shù)線程池實(shí)現(xiàn)都離不開鎖的使用,如互斥量pthread_mutex*結(jié)合條件變量pthread_cond*。眾所周知,鎖的使用對(duì)于程序性能影響較大,雖然現(xiàn)有的pthread_mutex*在鎖的申請(qǐng)與釋放方面做了較大的優(yōu)化,但是,線程池的實(shí)現(xiàn)是可以做到無(wú)鎖化的。
1.常見線程池實(shí)現(xiàn)原理

如上圖所示,工作隊(duì)列由主線程和工作者線程共享,主線程將任務(wù)放進(jìn)工作隊(duì)列,工作者線程從工作隊(duì)列中取出任務(wù)執(zhí)行。共享工作隊(duì)列的操作需在互斥量的保護(hù)下安全進(jìn)行,主線程將任務(wù)放進(jìn)工作隊(duì)列時(shí)若檢測(cè)到當(dāng)前待執(zhí)行的工作數(shù)目小于工作者線程總數(shù),則需使用條件變量喚醒可能處于等待狀態(tài)的工作者線程。當(dāng)然,還有其他地方可能也會(huì)使用到互斥量和條件變量,不再贅述。
2.無(wú)鎖化線程池實(shí)現(xiàn)原理

為解決無(wú)鎖化的問(wèn)題,需要避免共享資源的競(jìng)爭(zhēng),因此將共享工作隊(duì)列加以拆分成每工作線程一個(gè)工作隊(duì)列的方式。對(duì)于主線程放入工作和工作線程取出任務(wù)的競(jìng)爭(zhēng)問(wèn)題,可以采取環(huán)形隊(duì)列的方式避免。在解決了鎖機(jī)制之后,就只剩下條件變量的問(wèn)題了,條件變量本身即解決條件滿足時(shí)的線程通信問(wèn)題,而信號(hào)作為一種通信方式,可以代替之,其大體編程范式為:
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服務(wù)器架構(gòu)師學(xué)習(xí)資料私信“資料”(資料包括C/C++,Linux,golang技術(shù),Nginx,ZeroMQ,MySQL,redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協(xié)程,DPDK,ffmpeg等),免費(fèi)分享

3.無(wú)鎖化線程池具體實(shí)現(xiàn)
在無(wú)鎖線程池中,區(qū)別于常見線程池的地方主要在于信號(hào)與條件變量、任務(wù)調(diào)度算法、增加或減少線程數(shù)目后的任務(wù)遷移,另外還有一點(diǎn)就是環(huán)形隊(duì)列的實(shí)現(xiàn)參考了Linux內(nèi)核中的kfifo實(shí)現(xiàn)。
(1) 信號(hào)與條件變量
信號(hào)與條件變量的區(qū)別主要在于條件變量的喚醒(signal)對(duì)于接收線程而言可以忽略,而在未設(shè)置信號(hào)處理函數(shù)的情況下信號(hào)的接收會(huì)導(dǎo)致接收線程甚至整個(gè)程序的終止,因此需要在線程池產(chǎn)生線程之前指定信號(hào)處理函數(shù),這樣新生的線程會(huì)繼承這個(gè)信號(hào)處理函數(shù)。多線程中信號(hào)的發(fā)送主要采用pthread_kill,為避免使用其他信號(hào),本程序中使用了SIGUSR1。
(2) 任務(wù)調(diào)度算法
常見線程池實(shí)現(xiàn)的任務(wù)調(diào)度主要在操作系統(tǒng)一級(jí)通過(guò)線程調(diào)度實(shí)現(xiàn)。考慮到負(fù)載均衡,主線程放入任務(wù)時(shí)應(yīng)采取合適的任務(wù)調(diào)度算法將任務(wù)放入對(duì)應(yīng)的工作者線程隊(duì)列,本程序目前已實(shí)現(xiàn)Round-Robin和Least-Load算法。Round-Robin即輪詢式地分配工作,Least-Load即選擇當(dāng)前具有最少工作的工作者線程放入。
(3) 任務(wù)遷移
在線程的動(dòng)態(tài)增加和減少的過(guò)程中,同樣基于負(fù)載均衡的考量,涉及到現(xiàn)有任務(wù)的遷移問(wèn)題。負(fù)載均衡算法主要基于平均工作量的思想,即統(tǒng)計(jì)當(dāng)前時(shí)刻的總?cè)蝿?wù)數(shù)目,均分至每一個(gè)線程,求出每個(gè)工作者線程應(yīng)該增加或減少的工作數(shù)目,然后從頭至尾遍歷,需要移出工作的線程與需要移入工作的線程執(zhí)行任務(wù)遷移,相互抵消。最后若還有多出來(lái)的工作,再依次分配。遷入工作不存在競(jìng)態(tài),因?yàn)榧尤牍ぷ魇冀K由主線程完成,而遷出工作則存在競(jìng)態(tài),因?yàn)樵谶w出工作的同時(shí)工作者線程可能在同時(shí)執(zhí)行任務(wù)。所以需要采用原子操作加以修正,其主要思想即預(yù)取技術(shù),大致實(shí)現(xiàn)為:
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在線程的動(dòng)態(tài)減少后,原先線程上未能執(zhí)行完的任務(wù)只需要由
//主線程再次根據(jù)任務(wù)調(diào)度算法重新分配至其他存活的工作者線程隊(duì)列中即可,不
//存在上述問(wèn)題,當(dāng)然,此時(shí)可以同時(shí)執(zhí)行負(fù)載均衡算法加以優(yōu)化。
}
(4) 環(huán)形隊(duì)列
源碼中環(huán)形隊(duì)列實(shí)現(xiàn)主要參考了linux內(nèi)核中kfifo的實(shí)現(xiàn),如下圖所示:

隊(duì)列長(zhǎng)度為2的整次冪,out和in下標(biāo)一直遞增至越界后回轉(zhuǎn),其類型為unsigned int,即out指針一直追趕in指針,out和in映射至FiFo的對(duì)應(yīng)下標(biāo)處,其間的元素即為隊(duì)列元素。