根據(jù) 《0 基于socket和pthread實(shí)現(xiàn)多線程服務(wù)器模型》所述,server創(chuàng)建子線程的時(shí)候用的是以下代碼:
pconnsocke?=?(int?*)?malloc(sizeof(int));
*pconnsocke?=?new_fd;
ret?=?pthread_create(&tid,?NULL,?rec_func,?(void?*)?pconnsocke);
if?(ret?<?0)
{
perror("pthread_create?err");
return?-1;
}
為什么必須要malloc一塊內(nèi)存專門(mén)存放這個(gè)新的套接字呢?
要講清楚這個(gè)問(wèn)題的原因需要一些背景知識(shí):
- Linux創(chuàng)建一個(gè)新進(jìn)程時(shí),新進(jìn)程會(huì)創(chuàng)建一個(gè)主線程;
- 每個(gè)用戶進(jìn)程有自己的地址空間,系統(tǒng)為每個(gè)用戶進(jìn)程創(chuàng)建一個(gè)task_struct來(lái)描述該進(jìn)程,實(shí)際上task_struct 和地址空間映射表一起用來(lái),表示一個(gè)進(jìn)程;
- Linux里同樣用task_struct來(lái)描述一個(gè)線程,線程和進(jìn)程都參與統(tǒng)一的調(diào)度;
- 進(jìn)程內(nèi)的不同線程執(zhí)行是同一程序的不同部分,各個(gè)線程并行執(zhí)行,受操作系統(tǒng)異步調(diào)度;
- 由于進(jìn)程的地址空間是私有的,因此在進(jìn)程間上下文切換時(shí),系統(tǒng)開(kāi)銷(xiāo)比較大;
- 在同一個(gè)進(jìn)程中創(chuàng)建的線程共享該進(jìn)程的地址空間。
明白這些基礎(chǔ)知識(shí)后,下面我來(lái)看下,當(dāng)進(jìn)程創(chuàng)建一個(gè)子線程的時(shí)候,傳遞的參數(shù)情況:
直接傳遞棧中內(nèi)存地址
我們首先分析下如果創(chuàng)建子線程傳遞的是局部變量new_fd的地址這種情況。
由上圖所示:
- 創(chuàng)建一個(gè)線程,如果我們按照?qǐng)D中傳遞參數(shù)方法,那么new_fd是在棧中的,創(chuàng)建子線程的時(shí)候我們把new_fd地址傳遞給了thread1,線程回調(diào)參數(shù)arg的地址是new_fd地址。
- 因?yàn)橹骱瘮?shù)會(huì)一直循環(huán)不退出,所以new_fd一直存在棧中。用這種方法的確可以把new_fd的值3傳遞到子線程的局部變量fd,這樣子線程就可以使用這個(gè)fd與客戶端通信。
- 但是因?yàn)槲覀冊(cè)O(shè)計(jì)的是并發(fā)服務(wù)器模型,我們沒(méi)有辦法預(yù)測(cè)客戶端什么時(shí)候會(huì)連接我們的服務(wù)器,假設(shè)遇到一個(gè)極端情況,在同一時(shí)刻,多個(gè)客戶端同時(shí)連接服務(wù)器,那么主線程是要同時(shí)創(chuàng)建多個(gè)子線程的。
多個(gè)客戶端同時(shí)連接服務(wù)器
如上圖所示,所有新建的的thread回調(diào)函數(shù)的參數(shù)arg存放的都是new_fd的地址。如果客戶端連接的時(shí)候時(shí)間間隔比較大,是沒(méi)有問(wèn)題的,但是在一些極端的情況下還是有可能出現(xiàn)由于高并發(fā)引起的錯(cuò)誤。
我們來(lái)捋一下極端的調(diào)用時(shí)序:
第一步:
如上圖所示:
- T1時(shí)刻,當(dāng)客戶端1連接服務(wù)器的時(shí)候,服務(wù)器的accept函數(shù)會(huì)創(chuàng)建新的套接字4;
- T2時(shí)刻,創(chuàng)建了子線程thread1,同時(shí)子線程回調(diào)函數(shù)參數(shù)arg指向了棧中new_fd對(duì)應(yīng)的內(nèi)存。
- 假設(shè),正在此時(shí),又有一個(gè)客戶端要連接服務(wù)器,而且thread1頁(yè)已經(jīng)用盡了時(shí)間片,那么主線程server會(huì)被調(diào)度到。
第二步:
如上圖所示:
- 5T3時(shí)刻,主線程server接受了客戶端的連接,accept函數(shù)會(huì)創(chuàng)建新的套接字5,同時(shí)創(chuàng)建子線程thread2,此時(shí)OS調(diào)度的thread2;
- T4時(shí)刻,thread2通過(guò)arg得到new_fd了的值5,并存入fd;
- T5時(shí)刻,時(shí)間片到了,調(diào)度thread1,thread1通過(guò)arg去讀取new_fd,此時(shí)棧中new_fd的值已經(jīng)修5覆蓋了;
- 所以出現(xiàn)了2個(gè)線程同時(shí)使用同一個(gè)fd的情況發(fā)生。
這種情況的發(fā)生,雖然概率很低,但是并不代表不發(fā)生,該bug就是一口君在解決實(shí)際項(xiàng)目中遇到過(guò)的。
傳遞堆內(nèi)存地址
如果采用傳遞堆的地址的方式,我們看下圖:
- T1時(shí)刻,當(dāng)客戶端1連接服務(wù)器的時(shí)候,服務(wù)器的accept函數(shù)會(huì)創(chuàng)建新的套接字4,在堆中申請(qǐng)一塊內(nèi)存,用指針pconnsocke指向該內(nèi)存,同時(shí)將4保存到堆中;
- T2時(shí)刻,創(chuàng)建了子線程thread1,同時(shí)子線程回調(diào)函數(shù)參數(shù)arg指向了堆中pconnsocke指向的內(nèi)存。
- 假設(shè),正在此時(shí),又有一個(gè)客戶端要連接服務(wù)器,而且thread1頁(yè)已經(jīng)用盡了時(shí)間片,那么主線程server會(huì)被調(diào)度到。
- T3時(shí)刻,主線程server接受了客戶端的連接,accept函數(shù)會(huì)創(chuàng)建新的套接字5,在堆中申請(qǐng)一塊內(nèi)存,用指針pconnsocke指向該內(nèi)存,同時(shí)將5保存到堆中,然后創(chuàng)建子線程thread2;
- T4時(shí)刻,thread2通過(guò)arg指向了堆中pconnsocke指向的內(nèi)存,此處值為5,并存入fd;
- T5時(shí)刻,時(shí)間片到了,調(diào)度thread1,thread1通過(guò)arg去讀取fd,此時(shí)堆中數(shù)據(jù)位5;
- 就不會(huì)出現(xiàn)了2個(gè)線程同時(shí)使用同一個(gè)fd的情況發(fā)生。
這個(gè)知識(shí)點(diǎn)有點(diǎn)隱蔽,希望讀者在使用的時(shí)候多加小心。下一章,我們要講解如何利用我們現(xiàn)有的代碼實(shí)現(xiàn)登錄注冊(cè)的功能。