您當前的位置:首頁 > 遊戲

C 語言執行緒間怎麼通訊?

作者:由 知乎使用者 發表于 遊戲時間:2022-02-22

C 語言執行緒間怎麼通訊?碼農的荒島求生2022-03-10 22:27:10

首先糾正一下題主的描述,同一個程序內部的執行緒不存在“通訊”這個問題的,原因很簡單,一個程序內部的執行緒共享該程序的地址空間,因此這些執行緒天然可以直接訪問彼此的資料,因此根本不需要“通訊”一說。

題主描述的問題在多執行緒語境下有一個專門的描述,這不叫執行緒通訊而是叫做執行緒同步,你在作業系統課上學的暈頭轉向的並行流的同步互斥問題說的就是這。

回到題主的問題,有一個執行緒在計算結果,有一個執行緒要對結果進行處理,那麼顯然一個執行緒是資料的“生產者”,另一個執行緒是資料的“消費者”,這是不是一個非常經典的生產者消費者問題:

C 語言執行緒間怎麼通訊?

注,上圖出自《你管這破玩意叫執行緒?》一文。

作業系統課上是不是有專門講解過這個問題,上課沒好好聽講的要反思了。

實際上很簡單,生產者執行緒生產的資料可以直接放到佇列中,消費者執行緒從佇列中取出資料處理即可,這裡的難點在於多執行緒需要同時讀寫佇列,因此這裡出現了兩個問題:

1,確保佇列的互斥訪問

2,佇列空時消費者執行緒不可以讀,佇列滿是生產者執行緒不可以寫

PV操作專門用來解決這個問題,隨便翻一本作業系統教材都能找到答案。

C 語言執行緒間怎麼通訊?invalid s2022-03-11 16:16:29

你缺的東西還挺多的。

第一,同一個程序內部的執行緒間不存在通訊問題,想怎麼訪問怎麼訪問;所以我們反而需要做一些事,從而主動“隔離”不同執行緒,避免資料髒讀髒寫。

第二,多執行緒程式設計(以及多程序程式設計)都需要作業系統方面的底子。不懂作業系統,多執行緒協作是做不好的。

具體到你這個案例上,簡單說,不要輪詢。

輪詢這個動作本身就決定了,你的程式必定CPU佔用奇高、發熱巨大,同時執行緩慢。

這還是程式邏輯過於簡單;稍微複雜一點,你這種寫法,最終必然是“CPU佔用跑滿,程式邏輯寸步不前”,和一個死迴圈的垃圾沒有什麼差別。

第一步,先設定一個全域性的、標準的鎖(mutex)。

注意,第一個執行緒要修改記憶體資料,需要先申請鎖,確保第二個執行緒不在讀取資料;

第二個執行緒發現數據可用,也要先申請鎖,確保第一個執行緒不會繼續修改它。

也就是類似你過去那個“全域性變數”的作用;但一定要使用標準的鎖、使用標準的acquire系統呼叫申請鎖資料讀寫許可權。

這是因為,標準的mutex是作業系統提供的;當你的某個執行緒申請mutex失敗時,作業系統會把它置於等待佇列,在mutex可用前不會繼續給它分配時間片,這就避免了忙等;而一旦mutex可用,這個執行緒就會被移回就緒佇列,之後就可能獲得時間片了。

這就避免了大量無效的CPU佔用。

第二步,認真分析業務邏輯,畫出兩個執行緒的狀態切換圖,確定鎖應該有幾個、分別是什麼狀態(比如是否需要讀寫鎖);確保“執行緒申請到鎖就一定可以執行;執行緒無法執行就一定要進入掛起狀態”。

注意,你並不能確定什麼時候第二個執行緒正在讀取資料、或者阻塞在哪裡長時間沒有讀取。所以你必須使用足夠多的標誌位,確保“資料未初始化、資料初始化中、資料初始化完成等待讀取、資料讀取中、資料讀取完成”等狀態可清晰區分。否則,資料就可能丟失(執行緒一產生資料後,執行緒二尚未得到排程,執行緒一又用新資料覆蓋了之前的資料)或者出現髒讀、髒寫。

當然,視業務需要,只有true/false兩個狀態的鎖也許已經夠用了,但你必須認真評估、充分討論之後再這麼做——你的問題描述過於簡略,無法確定是否能行。

第三步,重新設計共享資料結構,把“鎖定時間”降到最低。

從你的描述中可知,執行緒1是不能停的,需要“不斷的生成計算結果”;但如此一來……

而mutex的預設行為是:申請不到鎖,就把申請鎖的執行緒掛起。

於是,執行緒1生成計算結果時,執行緒2只能等著;而執行緒2處理計算結果那5ms,執行緒1也只能等著……

萬一作業系統再安排不了時間片,那執行緒1可能就得等200ms,執行緒2才得到執行權;執行緒2執行時,執行緒1進了等待佇列,等執行緒2釋放鎖,執行緒1才移回就緒佇列,又等了200ms才得以執行……也就是出現了一個400ms以上的大卡頓。

這樣搞的話,你其實根本就不應該用什麼多執行緒。直接放在同一個執行緒裡,收集50ms的資料,然後執行5ms的處理——簡單,又不容易出錯,效率高,響應快……

想要借多執行緒提高吞吐率,那麼就必須搞一個更好用的資料結構。

比如,一個連結串列。

連結串列的每個節點足夠容納50ms的資料;執行緒1先申請一個節點,把資料寫進去,寫50ms後,申請這個全域性連結串列的鎖,把資料掛進連結串列——鎖定期間只需執行一條把連結串列末端next指向新節點的操作(可能還需要維護一下頭尾指標,不要每次都順著連結串列摸到尾)。

類似的,執行緒2被排程後,申請鎖定連結串列,然後把鏈條第一個節點移除、指標記錄在本地,隨即釋放鎖;然後就可以不受打擾的處理這個節點攜帶的資料了。

注意,這時候,如果還用最簡單的mutex的話,因為所有關於資料結構(連結串列)的操作都需要先鎖定,再檢查有無資料;那麼執行緒2可能就會死迴圈的不停上鎖、檢查發現沒資料,釋放鎖,然後馬上又上鎖……也就是絕大部分執行時間都在加解鎖上。

所以,這時候我們就不得不搞一個更復雜的東西,比如,讓mutex包含多個值。

當mutex非0時,執行緒2才可以從連結串列取出節點、同時把mutex值減一,減到0執行緒2就必須休眠,不要再去訪問連結串列;而執行緒1每成功往連結串列加入一個節點,就把mutex值加一……

但這時候,由於執行緒1/2的讀寫可能很頻繁,如果鎖定之後才讀寫資料的話,那麼鎖定時間就會是50ms/5ms,允許另一個執行緒訪問的時間就會特別特別短(比如每50ms/5ms解鎖若干個ns,也就是超過90%以上的時間裡資料都在鎖定狀態);這時候另一個執行緒實際上是拿不到資料的,因為作業系統必須恰巧在第一個執行緒解鎖後的若干納秒裡切換時間片、且剛好輪到它執行——除非其中一個執行緒一口氣把緩衝區寫滿、或者把所有緩衝資料處理完然後陷入阻塞,否則另一個執行緒可能永遠得不到執行機會。這就是術語說的“餓死”,是必須避免的。

因此,請最佳化演算法、最佳化資料結構,把資料準備/處理放在鎖定時間之外。如此一來,鎖定後可以只處理一下next/head/tail指標,把節點掛入/取下,然後就馬上釋放鎖。

換句話說,只有想辦法把共享資料弄的“在大部分時候可用”,兩個執行緒才能協作起來。

以上完成後,你可以進一步把這個共享資料結構實現成一個通用的、支援多執行緒訪問的佇列,只允許透過pop/push介面訪問資料;同時把加解鎖放到這兩個接口裡,從而簡化使用邏輯,杜絕錯誤訪問。

事實上,你的這個案例可能還可以進一步最佳化。

比如,如果只有這麼兩個執行緒,且執行緒1是生產者執行緒2是消費者(單生產者/消費者模型),那麼這裡甚至可以不用鎖,實現一個標準的環形陣列即可——這也是經典的、最簡化的無鎖程式設計案例。

當然,這樣做之前,請確認你的軟體執行平臺(CPU)明確宣告“指標訪問是原子操作”,否則……

如果參與者更多、邏輯更復雜,那麼鎖就是必需的;甚至讀寫鎖、旗語、event等東西都必須全面利用起來。

這個就太複雜了,這裡一時講不清,還是自己去看作業系統原理的相關章節吧。

C 語言執行緒間怎麼通訊?大寬寬2022-03-11 18:24:23

共享變數+mutex就行了吧……

如果一個變數不夠用,搞個blockingQueue就行

C 語言執行緒間怎麼通訊?嵌入式Linux2022-03-14 11:29:13

謝謝邀請

很多答主都給出了不錯的解決思路,我就沒必要新增太多一樣的觀點。

我說一個在專案上比較常見的思路,在很多音影片上應用比較多的方法。

C 語言執行緒間怎麼通訊?

題目設定一個變數的做法,

這樣僅僅是完成了通知上的隔離,

也就是說,資料處理完成了,我通知你,讓你從我這把資料拿走,在Linux程式設計種,有很多通知的機制。

緩衝區隔離區

緩衝區可以認為是一個環形佇列,佇列儲存的既可以是資料也可以是變數標誌位

至於答主所說的

CPU佔用高確實是個問題

,所以對緩衝區修改成阻塞方式,會是一個很不錯的方式。

C 語言執行緒間怎麼通訊?Xi Yang2022-03-23 13:07:36

題主你差了很多東西。由於現代CPU的複雜性,你要是裸著共享資料會以很奇怪的姿勢崩的。

執行緒間通訊(或者同步)的基本“套路”,主要有這些:

最常用的跨執行緒設施,一個用來表達互斥的物件。在某一時刻,只有一個執行緒能處於“獲得了鎖”的狀態,其它執行緒都會卡在“去獲得鎖”的這步,直到鎖被釋放,才會有另一個執行緒獲得。鎖通常用來保護一段程式碼在同一時刻只被一個執行緒執行。

鎖通常還會有一些特性:

能不能“重入”:能重入的鎖,同一個執行緒在鎖住的時候再呼叫“鎖”命令,不會把自己卡在那。沒有這個特性,同一個執行緒會卡住自己。能重入的鎖會便於編寫複雜程式碼,但是鎖本身會變得複雜。

是否涉及排程器:鎖住的執行緒是在死迴圈還是通知系統排程器把自己切出去。前者實時性好,但是被鎖住時效能非常糟糕。後者反之。

條件變數

執行緒可以“等待”在這裡,直到別人通知“關門放狗”,才會放過一個或者所有的等待執行緒。

跨執行緒管道

可以一個(或幾個)執行緒往裡面塞東西,另一個(或幾個)執行緒往外取東西。可以依此來分發任務給工作執行緒。

訊號量

可以用來表示資源與消費者的數量,比如零為初始狀態,整數表示有這麼多個可用資源,負數表示有這麼多個消費者執行緒在等待。

原子操作

一些保證了“一定被同一個執行緒完成,其它執行緒要麼讀到操作前狀態,要麼讀到操作後狀態”的組合操作,實際上會呼叫CPU的一些專用同步指令。主要包含這些:

讀/寫:好像在x86架構裡意義不大,因為我記得x86的讀寫本身天然就是原子的。

自加/自減。

compare-and-swap:如果目標等於比較數值,就把它設成新的數值,並且把原來儲存的數值返回給你。這個操作擁有最高的consensus value,可以(理論上)區分無窮多個執行緒狀態。

原子操作擁有最高的程式設計靈活性,實際上大部分跨執行緒設施都是用原子操作實現的。

這些玩意裡面,很多東西都同時會有作業系統的實現(比如鎖和條件變數通常都會作為系統API的一部分),也會有庫實現,也會有編譯器擴充套件實現(比如原子操作)。