根據 《0 基于socket和pthread實作多執行緒服務器模型》所述,server創建子執行緒的時候用的是以下代碼:
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;
}
獲取更多關于Linux的資料,請關注公眾號「一口Linux」
為什么必須要malloc一塊記憶體專門存放這個新的套接字呢?
要講清楚這個問題的原因需要一些背景知識:
- Linux創建一個新行程時,新行程會創建一個主執行緒;
- 每個用戶行程有自己的地址空間,系統為每個用戶行程創建一個task_struct來描述該行程,
實際上task_struct 和地址空間映射表一起用來,表示一個行程; - Linux里同樣用task_struct來描述一個執行緒,執行緒和行程都參與統一的調度;
- 行程內的不同執行緒執行是同一程式的不同部分,各個執行緒并行執行,受作業系統異步調度;
- 由于行程的地址空間是私有的,因此在行程間背景關系切換時,系統開銷比較大;
- 在同一個行程中創建的執行緒共享該行程的地址空間,
明白這些基礎知識后,下面我來看下,當行程創建一個子執行緒的時候,傳遞的引數情況:
直接傳遞堆疊中記憶體地址
我們首先分析下如果創建子執行緒傳遞的是區域變數new_fd的地址這種情況,

由上圖所示:
-
創建一個執行緒,如果我們按照圖中傳遞引數方法,那么new_fd是在堆疊中的,創建子執行緒的時候我們把new_fd地址傳遞給了thread1,執行緒回呼引數arg的地址是new_fd地址,
-
因為主函式會一直回圈不退出,所以new_fd一直存在堆疊中,用這種方法的確可以把new_fd的值3傳遞到子執行緒的區域變數fd,這樣子執行緒就可以使用這個fd與客戶端通信,
-
但是因為我們設計的是并發服務器模型,我們沒有辦法預測客戶端什么時候會連接我們的服務器,假設遇到一個極端情況,在同一時刻,多個客戶端同時連接服務器,那么主執行緒是要同時創建多個子執行緒的,
多個客戶端同時連接服務器

如上圖所示,所有新建的的thread回呼函式的引數arg存放的都是new_fd的地址,如果客戶端連接的時候時間間隔比較大,是沒有問題的,但是在一些極端的情況下還是有可能出現由于高并發引起的錯誤,
我們來捋一下極端的呼叫時序:
第一步:

如上圖所示:
- T1時刻,當客戶端1連接服務器的時候,服務器的accept函式會創建新的套接字4;
- T2時刻,創建了子執行緒thread1,同時子執行緒回呼函式引數arg指向了堆疊中new_fd對應的記憶體,
- 假設,正在此時,又有一個客戶端要連接服務器,而且thread1頁已經用盡了時間片,那么主執行緒server會被調度到,
第二步:

如上圖所示:
5. T3時刻,主執行緒server接受了客戶端的連接,accept函式會創建新的套接字5,同時創建子執行緒thread2,此時OS調度的thread2;
6. T4時刻,thread2通過arg得到new_fd了的值5,并存入fd;
7. T5時刻,時間片到了,調度thread1,thread1通過arg去讀取new_fd,此時堆疊中new_fd的值已經修5覆寫了;
8. 所以出現了2個執行緒同時使用同一個fd的情況發生,
這種情況的發生,雖然概率很低,但是并不代表不發生,該bug就是一口君在解決實際專案中遇到過的,
傳遞堆記憶體地址
如果采用傳遞堆的地址的方式,我們看下圖:

- T1時刻,當客戶端1連接服務器的時候,服務器的accept函式會創建新的套接字4,在堆中申請一塊記憶體,用指標pconnsocke指向該記憶體,同時將4保存到堆中;
- T2時刻,創建了子執行緒thread1,同時子執行緒回呼函式引數arg指向了堆中pconnsocke指向的記憶體,
- 假設,正在此時,又有一個客戶端要連接服務器,而且thread1頁已經用盡了時間片,那么主執行緒server會被調度到,
- T3時刻,主執行緒server接受了客戶端的連接,accept函式會創建新的套接字5,在堆中申請一塊記憶體,用指標pconnsocke指向該記憶體,同時將5保存到堆中,然后創建子執行緒thread2;
- T4時刻,thread2通過arg指向了堆中pconnsocke指向的記憶體,此處值為5,并存入fd;
- T5時刻,時間片到了,調度thread1,thread1通過arg去讀取fd,此時堆中資料位5;
- 就不會出現了2個執行緒同時使用同一個fd的情況發生,
這個知識點有點隱蔽,希望讀者在使用的時候多加小心,
下一章,我們要講解如何利用我們現有的代碼實作登錄注冊的功能,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/22003.html
標籤:java
