倉庫:https://gitee.com/mrxiao_com/2d_game_3
回顧/復習
今天是繼續完善和調試多線程的任務隊列。之前的幾天,我們已經介紹了多線程的一些基礎知識,包括如何創建工作隊列以及如何在線程中處理任務。今天,重點是解決那些我們之前沒有注意到的問題,并確保任務隊列能在多線程環境下正確運行。
昨天,我們寫了一個非常簡單且不安全的工作隊列版本,這個版本可以將任務(比如字符串)推入隊列,并由工作線程來處理這些任務。但是,這個簡單實現沒有考慮到多線程的一些關鍵問題,結果在運行時出現了各種不穩定和錯誤的現象,比如多個線程同時處理同一個任務,或者有任務根本沒有被處理。
具體來說,問題在于工作隊列的設計缺乏多線程安全性。當多個線程并發訪問共享資源時,如果沒有適當的同步機制,就會發生數據競爭,導致某些任務被跳過或重復執行。這些問題在簡單的實現中顯現出來,并且表現得不穩定和不可預測。
現在,任務是改進這個不安全的實現,利用之前提到的多線程基礎知識(如內存屏障、互鎖操作等),來確保工作隊列能夠安全有效地在多線程環境中運行。目標是讓所有的工作單元都能被線程正確處理,并且每個工作單元只處理一次,確保每個任務都能按預期執行。
通過這些改進,我們希望解決線程競爭和任務分配不均的問題,從而使多線程代碼在實際應用中能夠正常、高效地工作。
昨天的待辦事項
首先,我們需要解決一個問題,這個問題在某些處理器上可能并不存在,比如在x64處理器上。這個問題本質上是一個編譯器相關的問題,但不管怎樣,它依然是一個實際的問題,我們需要解決。
具體來說,問題出在工作隊列的寫入順序上。在現有的代碼中,我們試圖將任務寫入工作隊列。這包括將數據寫入隊列的內容部分,然后再對隊列的入口計數進行遞增,表示工作隊列中有一個新的任務條目。但問題是,遞增計數的操作發生在我們實際寫入任務數據之前。這意味著,如果有其他線程正在檢查工作隊列,它們可能會看到這個計數器已經遞增,但實際上任務數據還沒有寫入隊列。也就是說,任務計數器的增量操作會先于任務數據的寫入,這就可能導致其他線程錯誤地認為隊列已經有了一個新的任務,而實際上任務還沒有完全準備好。
為了避免這種情況,我們需要確保對工作隊列的寫入和計數的更新是嚴格按順序執行的,確保計數器只在數據完全寫入后才遞增,這樣可以避免其他線程讀取到不一致的狀態。這個問題涉及到操作的順序性,需要通過適當的同步機制來保證數據的一致性。
待辦事項 1:寫入順序
在這個過程中,要確保代碼更加健壯,首先需要確保在執行任何操作之前,先把數據寫入。這意味著,在寫入工作隊列之前,必須確保相關數據已經正確寫入,然后再增加 EntryCount
的值。然而,盡管重新排列了代碼行,這種做法仍然不完全有效。
原因在于,盡管處理器可能會保證按指令順序執行寫操作(即確保寫操作按順序進行),但是編譯器并沒有義務保持寫操作的順序。優化后的編譯器可能會重新排序代碼,比如將增加 EntryCount
的操作提前到寫字符串之前,以提高效率或其他原因。這樣的話,代碼可能在編譯階段就被打破了,處理器的寫順序保證也就沒有意義了。
這個問題源于不同處理器在處理寫操作時的行為。并不是所有的處理器都能保證按順序執行寫操作。有些處理器可能會允許出于性能考慮將寫操作亂序執行。因此,了解特定平臺的處理器行為非常重要。
要解決這些問題,可以采用一些技術來確保編譯器不會重排寫操作。我們需要在代碼中插入指示,告訴編譯器不要將一個寫操作推遲到已經發生的其他寫操作之后。這通常可以通過內存屏障(memory barrier)或類似機制來實現,確保編譯器按正確的順序執行寫操作,而不是做不必要的優化。
這樣,通過保證寫操作的順序,可以確保多線程程序在處理共享數據時不會出現競爭條件和錯誤行為。
完成過去的寫入操作,再進行未來的寫入操作
為了確保代碼在多線程環境下更加健壯,需要確保“過去的寫操作”在“未來的寫操作”之前完成。為了解決這個問題,可以采用宏來確保在不同平臺上執行正確的操作。
首先,針對編譯器,主要問題是它沒有強制要求按順序執行寫操作。為了讓編譯器保持正確的寫順序,可以通過內存屏障(memory barrier)來解決。在某些平臺上,處理器本身可能會執行寫操作的強順序,但編譯器可能會優化代碼,改變寫操作的順序,這樣就可能導致問題。因此,在宏中,除了告訴編譯器按順序執行寫操作外,如果在某些平臺上處理器沒有強順序保證,我們還需要加入內存屏障指令。
例如,x64 處理器通常不需要額外的強制寫順序指令,因為它本身就保證寫操作順序。但在其他平臺上(如某些舊的架構),可能需要顯式告訴處理器按順序執行寫操作,這時就需要插入內存屏障指令來強制執行順序。
在Visual Studio中,針對這類問題可以使用 atomic_thread_fence
或者 std::atomic
的 C++ 語言特性。然而,出于對C++特性的信任問題,決定使用更底層的寫屏障指令來避免這些高層次的抽象,以確保在編譯階段不會對指令順序做不必要的優化。
具體來說,可以通過 write barrier
來實現,它的作用是防止在該點之前的寫操作被移動到之后,從而確保寫操作順序不被破壞。這個屏障并不是一個實際的函數調用,而只是一個標記,告訴編譯器在這個點不能重排寫操作。
最后,回到處理器本身,如果在一些平臺上確實需要顯式保證寫順序,可以使用內存屏障指令來告訴處理器序列化這些操作,確保寫操作不會被重新排序。
atomic_thread_fence
是 C++ 中用于同步線程間操作的一種原子操作,它屬于 C++11 標準引入的 std::atomic
庫。這個操作不會改變內存的內容或數據,它的作用是保證在它之前的所有操作(如讀寫操作)都完成,并且它之后的操作不會被提前執行。
atomic_thread_fence
的作用
它主要用于在多線程環境中管理內存訪問的順序。通過插入屏障(memory fence),atomic_thread_fence
告訴編譯器或處理器在該點之前和之后的操作必須按順序執行。
主要用途
-
防止內存重排序: 在多線程程序中,操作可能會被編譯器或處理器重排,從而導致不同線程間的同步問題。
atomic_thread_fence
可以防止這些重排,確保執行的順序。 -
內存屏障: 它可以用于創建內存屏障,防止在某些操作之后的指令被過早執行,或者某些操作的結果被推遲到屏障之后才執行。
參數
atomic_thread_fence
具有以下幾種常用的內存順序類型(memory order):
memory_order_relaxed
: 不強制任何順序。memory_order_consume
: 只保證該操作的結果對后續操作有影響(消費性順序)。memory_order_acquire
: 保證該操作之前的所有操作在該操作之前完成(獲取順序)。memory_order_release
: 保證該操作之后的所有操作在該操作之后完成(釋放順序)。memory_order_acq_rel
: 同時包含獲取和釋放順序。memory_order_seq_cst
: 強制順序,保證所有操作按照程序的順序執行。
示例
#include <atomic>void producer() {// 發布數據前,使用內存屏障來確保寫操作不會被重排序std::atomic_thread_fence(std::memory_order_release);shared_data = 42; // 共享數據
}void consumer() {// 在讀取共享數據之前,使用內存屏障來確保數據讀取的正確性std::atomic_thread_fence(std::memory_order_acquire);int x = shared_data; // 讀取共享數據
}
在這個示例中,producer
在寫入共享數據前使用了 memory_order_release
,確保所有先前的寫操作不會被重排到這之后。而 consumer
在讀取共享數據前使用了 memory_order_acquire
,確保數據讀取操作不會被重排到這之前。
什么時候使用 atomic_thread_fence
atomic_thread_fence
是一個底層的同步工具,通常用于實現線程同步機制,比如條件變量、鎖等。它確保線程之間的操作順序符合程序的預期,防止內存順序錯誤,尤其是在并發環境中,避免出現數據競爭或未定義行為。
需要注意的是,atomic_thread_fence
只是在線程之間同步內存訪問順序,并不會直接同步線程本身的執行。如果需要更復雜的線程同步(例如等待一個線程完成某個任務),需要使用諸如互斥鎖、條件變量等更高層的同步機制。
總結
atomic_thread_fence
是一個確保在多線程程序中內存操作順序的低級同步機制,能夠防止操作被過早或延遲執行。通過它,可以更精細地控制線程之間的同步,避免潛在的內存訪問錯誤。
警告
_ReadBarrier
、_WriteBarrier
、_ReadWriteBarrier
編譯器內建函數以及 MemoryBarrier
宏都已被棄用,不應再使用。對于線程間通信,請使用 C++ 標準庫中定義的機制,如 atomic_thread_fence
和 std::atomic<T>
。對于硬件訪問,請使用 /volatile:iso
編譯器選項,并配合 volatile
關鍵字使用。
查找內存屏障
在這段過程中,首先提到了需要使用內存屏障(memory fence)來防止加載和存儲操作的重新排序。實際操作中,期望能通過引入內存屏障來確保對內存的訪問順序。然而,遇到的問題是,雖然預期應該能看到內存屏障的相關指令(如在 Visual Studio 中的 _ReadBarrier
、_WriteBarrier
等),但這些并沒有按預期顯示出來,導致無法成功地插入內存屏障指令。
在嘗試解決這個問題時,發現有些標準的內存屏障函數或宏(如 _ReadWriteBarrier
)已經被棄用。建議采用 C++ 標準庫中的原子操作(如 atomic_thread_fence
)來進行線程間的同步和內存屏障操作。由于一段時間沒使用過這些庫,導致在代碼實現時,出現了對相關指令的遺忘,需要重新查閱文檔。
此外,作者也表示,自己已經很久沒有使用這些低級的內存屏障函數,因此現在需要重新學習并查找相關的指令,特別是在使用 x64 處理器時。對于內存屏障指令的使用,依賴于編譯器對內存操作的優化處理,但由于使用的工具和庫沒有立即顯示相關指令,因此需要進一步調查和修正問題。
插入一個實際的 CPU 屏障
如何確保編譯器和處理器都不會重新排序存儲操作。為了做到這一點,可以通過插入一個“內存屏障”來實現。這個內存屏障不僅僅是一個編譯器指令,它實際上會被插入到指令流中,使得處理器也會看到這個屏障,進而確保不會對屏障前后的存儲操作進行重排序。
內存屏障的作用是明確告訴編譯器,在這個屏障的位置之前的存儲操作不能被重新排序到后面。這對保證多線程程序中的同步性非常重要,因為不同線程之間的存儲操作順序需要得到正確的處理,避免出現不可預測的行為。
接著,提到了一些具體的代碼實現細節,并計劃檢查是否需要插入具體的處理器屏障。最后,做了一個提醒,可能需要進一步確認這一做法是否適用于當前的情況,并留下了一個待辦事項來處理相關的“存儲順序”的問題。
_mm_sfence()
和 _WriteBarrier
都是用來控制內存操作順序的工具,但它們在功能、使用場景以及底層實現上有所不同。以下是這兩者的區別:
1. 作用范圍與功能
-
_mm_sfence()
:_mm_sfence()
是一個低級的內存屏障,屬于 Intel 的 SSE 指令集(Streaming SIMD Extensions)。它是一個 store fence(寫入屏障)。- 它的作用是確保 所有在它之前的寫操作 都會在它之后的寫操作之前完成。這意味著,它不會讓之后的寫操作提前執行,而是強制先前的寫操作完成并刷新到內存。
- 主要用于硬件級別的內存順序控制,特別是在多處理器或多核心的環境下。
-
_WriteBarrier
:_WriteBarrier
是一個 編譯器指令,用于 告知編譯器不要重排序 寫操作。它并不直接插入硬件指令流,而是告訴編譯器“不要優化”代碼中指定位置的寫操作順序。- 它通常用于 防止編譯器對內存訪問進行重排,尤其是在并發程序中,確保編譯器不會亂序執行寫操作,這對于多線程環境中的同步問題至關重要。
2. 使用層次
-
_mm_sfence()
:_mm_sfence()
是一個硬件相關的指令,直接插入到程序的執行流中,會影響 CPU 執行的順序。它通常在 低級編程(如與硬件交互,或處理高性能計算)中使用。- 它與硬件的內存順序控制緊密相關,能夠直接影響處理器如何執行內存操作。
-
_WriteBarrier
:_WriteBarrier
是一種編譯器屏障,通常用于 高級編程 中,尤其是在多線程程序中控制內存序列。它作用于編譯階段,告訴編譯器在優化時不要改變指令的順序,而不影響處理器底層的內存操作。- 該指令用于控制編譯器的優化行為,而不是直接干預內存操作的硬件層級。
3. 實現機制
-
_mm_sfence()
:- 它實際上是一個 硬件指令,直接作用于 CPU 級別。在現代處理器中,
_mm_sfence()
會確保所有的寫操作按照程序中指定的順序完成。它通常用于 多核處理器 或 多線程環境,確保多個 CPU 核心之間的一致性。 - 它的作用是強制執行 內存屏障,確保在它前后的寫操作按順序執行。
- 它實際上是一個 硬件指令,直接作用于 CPU 級別。在現代處理器中,
-
_WriteBarrier
:- 它是 編譯器指令,影響的是 編譯器優化 行為。它告訴編譯器不要對寫操作進行重排序,避免因為編譯器的優化行為而改變程序的預期順序。
- 它不會直接操作硬件,而是依賴于編譯器生成的代碼來避免潛在的重排。
4. 應用場景
-
_mm_sfence()
:- 當需要強制 CPU 層面的內存屏障,尤其是在多核或多線程環境下,需要確保某些內存操作按順序執行時使用。
- 適用于對性能要求極高的程序,尤其是 低級系統編程 或 硬件交互編程,如操作系統開發、驅動開發或高性能計算程序中。
-
_WriteBarrier
:- 在多線程程序中,確保 編譯器不重排寫操作。在編寫并發程序時,尤其是 共享內存訪問 時,使用
_WriteBarrier
來確保程序邏輯的正確性。 - 適用于 高層次的多線程同步,而不需要直接干預硬件的執行行為。
- 在多線程程序中,確保 編譯器不重排寫操作。在編寫并發程序時,尤其是 共享內存訪問 時,使用
5. 平臺相關性
-
_mm_sfence()
:- 與 Intel/AMD x86 架構密切相關。它依賴于特定的硬件指令,因此在不同的硬件平臺上可能會有不同的效果。
-
_WriteBarrier
:- 它是與編譯器相關的指令,不依賴于特定的硬件架構。它的行為在不同的編譯器和編譯選項下可能有所不同,但其作用始終是防止編譯器對內存操作進行重排。
6. 簡潔對比
特性 | _mm_sfence() | _WriteBarrier() |
---|---|---|
類型 | 硬件內存屏障(CPU指令) | 編譯器屏障指令 |
作用 | 保證寫操作按順序執行 | 防止編譯器重排寫操作 |
使用場景 | 低級編程,高性能系統開發,硬件交互 | 高層次多線程編程,避免編譯器重排寫操作 |
影響范圍 | 直接影響 CPU 執行流 | 僅影響編譯器的優化行為 |
平臺相關性 | 與硬件平臺相關,主要用于 x86 架構 | 與編譯器和編譯器選項相關 |
適用性 | 多核/多線程環境中的內存順序控制 | 多線程編程中的編譯器優化控制 |
總結:
_mm_sfence()
是硬件級別的內存屏障,直接影響 CPU 執行,保證內存操作按順序完成,適用于低級系統編程和硬件交互。_WriteBarrier
是編譯器級別的屏障,控制編譯器的優化行為,防止編譯器重排寫操作,適用于多線程環境中的同步操作。
_mm_sfence()
是一個內存屏障(memory fence)函數,屬于 Intel 的 SIMD 指令集(Streaming SIMD Extensions, SSE)的一部分。它用于確保對存儲器的寫入操作按照指定的順序完成。
具體來說,_mm_sfence()
是一個 store fence(寫入屏障)。它的作用是確保在該指令之前的所有寫操作(store 操作)都已經被寫入到內存中,并且在該指令之后的寫操作不會在它之前執行。
主要作用:
- 確保寫入順序: 當調用
_mm_sfence()
時,它會阻止之后的寫操作(store 操作)被提前到這條指令之前執行,保證所有在它之前的寫操作先完成。 - 保證內存一致性: 適用于多線程程序中,確保多線程之間的數據寫入順序符合預期,以避免出現內存訪問的異常順序。
使用場景:
- 在多線程編程中,如果存在多個線程同時對共享數據進行寫入操作,
_mm_sfence()
可以確保一個線程對共享內存的寫入操作在另一個線程讀取之前完成。 - 常見于硬件編程、系統級編程以及高性能計算中。
示例:
#include <xmmintrin.h>void example() {// 先進行一系列的寫操作data1 = 42;data2 = 43;// 強制執行寫入順序_mm_sfence();// 后續的寫操作保證在之前的寫操作完成后執行data3 = 44;
}
注意事項:
_mm_sfence()
只影響 寫操作 的順序,對讀操作(load 操作)沒有影響。如果你需要確保讀取操作的順序,可能需要使用_mm_lfence()
或_mm_sfence()
的組合。- 現代編譯器通常會進行優化,因此需要謹慎使用內存屏障,確保它們只在必要時才使用。
總的來說,_mm_sfence()
主要用于確保多線程程序中的寫操作順序,從而避免并發執行時出現數據不一致的情況。
接下來我們該怎么辦?
目前已經做了一些改進,但是依然存在一個尚未解決的問題——編譯器方面的問題。雖然在代碼中加入了內存屏障(如 _mm_sfence()
或 WriteBarrier
),編譯器已經知道這些屏障的存在,但依然有一個潛在的問題未得到解決。之所以會有這個問題,是因為編譯器本身的行為沒有完全被考慮到。
即便加入了這些指令,編譯器可能仍然會對內存訪問進行優化,導致潛在的重排序問題。這個問題本質上源于編譯器優化和處理器執行的異步性,特別是在多線程環境下,編譯器并不總是嚴格遵守內存順序,甚至可能重新排列內存訪問的順序以提高執行效率。因此,盡管我們做了很多底層處理,仍然有可能出現競爭條件或同步錯誤。
同時,這里也提到,由于 C 語言本身并未專門為多線程設計,它本身的一些特性和行為會使得多線程編程更加復雜,特別是對于并發訪問的內存操作。需要特別注意編譯器如何優化代碼以及它如何影響內存操作的順序。
引入 volatile
在多線程編程中,編譯器的優化行為可能導致一些意料之外的錯誤。具體來說,編譯器在優化時可能會認為某些變量不會被其他線程修改,因此它可能會省略對這些變量的內存讀取,甚至將它們緩存到寄存器中,而不再每次訪問時都從內存中加載最新的值。這在多線程環境下會導致問題,因為另一個線程可能在執行過程中修改了這些變量。
為了解決這個問題,C 語言提供了一個關鍵字 volatile
。當聲明一個變量為 volatile
時,它告訴編譯器該變量可能會在沒有編譯器控制的情況下被改變,通常是由外部因素(如其他線程)修改。因此,編譯器不會對這個變量做優化,每次使用時都必須重新從內存加載它。這確保了多線程中變量的最新值不會被忽略,從而避免了線程間的數據競爭問題。
不過,并不是所有情況下都需要顯式使用 volatile
,因為在一些編譯器的默認行為中,某些對 volatile
變量的寫操作可能會自動插入寫屏障。寫屏障可以確保寫操作按順序執行,避免在多線程環境中重排操作。但需要注意的是,使用 volatile
并不直接插入任何機器級指令,它僅僅是告訴編譯器不進行優化,并保證內存的訪問順序。
所以,當程序的某些部分需要在多線程環境下保證數據一致性時,可能需要使用 volatile
來避免編譯器優化引發的錯誤,確保每次對共享變量的訪問都是最新的,從而避免并發問題。
這段描述還提到了,雖然 volatile
已經可以在某些情況下插入寫屏障,但在實際開發中,依然需要手動插入其他同步機制(如內存屏障),以確保在多線程環境下的正確性。
待辦事項 2:互鎖寫入
在多線程編程中,如果沒有使用適當的原子操作,就可能會出現競態條件的問題。例如,假設有兩個線程同時讀取同一個變量的值并分別對其進行遞增操作,最終它們可能都會讀取到相同的初始值,進行自增后再寫回,從而導致兩個線程都寫入相同的結果,造成數據不一致。
為了解決這個問題,需要使用原子操作(如互鎖操作)。互鎖操作是為了防止多個線程同時修改共享數據而引發沖突。具體來說,可以使用 InterlockedIncrement
函數來保證線程安全。
InterlockedIncrement
是一個非常簡單的原子操作,它會對指定的變量執行加一操作,并確保在多線程環境下每次只有一個線程能夠對這個變量進行修改。即使多個線程幾乎同時嘗試對變量進行加一操作,InterlockedIncrement
也會確保每個線程最終得到唯一的結果,從而避免了競態條件的發生。
雖然 InterlockedIncrement
足以解決這個問題,但由于它的功能比較簡單,有時也可以使用更復雜的 InterlockedCompareExchange
來替代它,這樣可以在某些情況下提供更多的控制和靈活性。InterlockedCompareExchange
可以進行條件比較和交換,因此適用于更復雜的并發操作。不過,在本例中,為了簡單起見,選擇使用 InterlockedIncrement
來演示,因為它更直觀且易于理解。
總結來說,InterlockedIncrement
確保了在多線程操作中,對同一變量的遞增操作是安全的,不會引起數據沖突,從而有效避免了由于并發執行導致的錯誤。
InterlockedIncrement
是 Windows API 中用于實現原子性遞增操作的函數,確保在多線程環境中多個線程并發修改同一變量時,不會出現競態條件。它通過硬件支持的原子操作來完成這個任務,確保即使多個線程同時對同一個變量進行遞增操作,也不會發生沖突,從而保持數據的一致性。
函數原型
LONG InterlockedIncrement(LONG volatile *Addend
);
參數
Addend
: 指向一個LONG
類型變量的指針,這個變量將被遞增。此變量在函數執行過程中是原子操作的目標。
返回值
- 返回遞增后的變量值。
說明
InterlockedIncrement
會原子地將 Addend
指向的值加 1,并返回遞增后的值。該操作確保了:
- 即使多個線程并發執行
InterlockedIncrement
,每個線程也會得到一個獨立且正確的遞增結果。 - 這個函數使用硬件支持的原子操作,因此不需要額外的鎖機制。
- 適用于需要多個線程安全地對一個共享計數器進行遞增的情況。
示例
#include <windows.h>
#include <iostream>int main() {LONG counter = 0;// 多線程環境下對 counter 進行原子遞增操作InterlockedIncrement(&counter);std::cout << "Counter after increment: " << counter << std::endl;return 0;
}
應用場景
- 計數器管理:例如,在多線程環境中用于線程安全地遞增計數器。可以確保每個線程在對計數器操作時,不會發生并發沖突,保證計數器的正確性。
- 任務分配:比如在任務調度系統中,多個線程同時請求任務時,使用
InterlockedIncrement
來確保任務編號遞增是線程安全的。 - 內存訪問控制:當多個線程需要在共享內存中進行原子性操作時,
InterlockedIncrement
可以有效避免數據競爭問題。
總結
InterlockedIncrement
是一個簡單、有效的原子操作函數,廣泛用于多線程編程中,確保線程在操作共享數據時不會發生競態條件,保持程序的線程安全性。
c++ 中atomic
在 C++ 中,std::atomic
提供了一些原子操作,包括原子遞增操作。使用 std::atomic
可以確保在多線程環境中對同一數據的操作是原子的,即避免了競態條件。
原子遞增操作
C++11 引入了 std::atomic
類模板,提供了各種原子操作。std::atomic
提供的原子遞增操作可以確保多個線程在并發訪問時不發生數據競爭。
std::atomic
原子遞增操作示例
1. 使用 fetch_add
進行原子遞增
fetch_add
是 std::atomic
提供的原子遞增函數,它會返回遞增之前的值,并對原子變量執行遞增操作。
#include <iostream>
#include <atomic>
#include <thread>std::atomic<int> counter(0);void increment_counter() {// 原子遞增counter.fetch_add(1, std::memory_order_relaxed);
}int main() {// 啟動多個線程進行遞增std::thread t1(increment_counter);std::thread t2(increment_counter);std::thread t3(increment_counter);t1.join();t2.join();t3.join();std::cout << "Counter after incrementing: " << counter.load() << std::endl;return 0;
}
2. 使用 operator++
進行原子遞增
std::atomic
還支持使用 ++
操作符直接進行原子遞增。操作符會自動調用 fetch_add
來實現原子遞增。
#include <iostream>
#include <atomic>
#include <thread>std::atomic<int> counter(0);void increment_counter() {// 原子遞增++counter;
}int main() {// 啟動多個線程進行遞增std::thread t1(increment_counter);std::thread t2(increment_counter);std::thread t3(increment_counter);t1.join();t2.join();t3.join();std::cout << "Counter after incrementing: " << counter.load() << std::endl;return 0;
}
主要函數與操作
-
fetch_add
:用于進行原子加法操作,遞增值并返回遞增前的值。可以指定內存順序(如std::memory_order_relaxed
、std::memory_order_acquire
等)來控制操作的順序。 -
operator++
:是對fetch_add(1)
的封裝,直接使用++
操作符進行原子遞增。 -
load
和store
:用于讀取和寫入原子變量的值。load
用于讀取,store
用于設置值。 -
exchange
:交換原子變量的值,返回交換前的值。
內存順序(Memory Order)
std::atomic
提供的原子操作支持內存順序的控制。默認的內存順序是 std::memory_order_seq_cst
,即順序一致性,保證操作按順序執行,但也可以使用其他內存順序來優化性能,如:
std::memory_order_relaxed
:不強制任何順序,僅保證原子操作。std::memory_order_acquire
:保證所有之前的操作在該操作之前完成。std::memory_order_release
:保證該操作之后的所有操作在此操作之后執行。std::memory_order_acq_rel
:同時具有 acquire 和 release 的效果。std::memory_order_seq_cst
:默認內存順序,確保全序一致性。
總結
使用 std::atomic
提供的原子遞增操作,能夠確保多線程環境下的數據一致性,并防止競態條件。通過原子操作,我們可以安全地進行遞增、交換等操作,而無需顯式加鎖。
查找 InterlockedIncrement
在這段討論中,主要講解了如何使用 InterlockedIncrement
函數來實現原子遞增操作,以確保在多線程環境中不會出現數據競爭。
-
InterlockedIncrement
介紹:InterlockedIncrement
是一種原子操作,用于確保在多線程環境中對共享變量的遞增操作不會出現競態條件。它會確保操作是線程安全的,即便多個線程同時操作同一個變量。- 它的工作原理是:該函數接收一個指向
volatile long
類型的指針,并對該變量進行原子遞增。由于volatile
關鍵字的使用,編譯器知道該變量可能被其他線程修改,因此不會優化掉對該變量的訪問。
-
遞增操作的細節:
InterlockedIncrement
會對指定的變量進行遞增,并返回遞增后的結果。然而,在某些情況下,我們需要知道遞增之前的值,而不是遞增之后的結果。這意味著在使用InterlockedIncrement
之后,我們得到的是遞增后的值,而不是遞增前的值。- 為了得到遞增前的值,可以在調用
InterlockedIncrement
后,將返回值減去1。這樣就能獲得遞增操作之前的原始值。
-
問題解決:
- 使用
InterlockedIncrement
確保了遞增操作的原子性,從而避免了多線程環境中出現數據競爭問題。這樣,即使多個線程同時進行遞增操作,每個線程都會看到正確的、獨立的值,避免了兩個線程同時看到相同的遞增結果。
- 使用
-
總結:
- 使用
InterlockedIncrement
可以有效地解決多線程環境下的競態條件問題,確保每個線程對共享變量的修改都是原子的。通過volatile
關鍵字,編譯器能夠正確處理并發操作,避免因優化而出現問題。
- 使用
待辦事項 3:已經由 volatile 處理
在這段討論中,主要講解了如何通過使用 volatile
關鍵字解決了一個潛在的問題,使得編譯器能夠正確處理多線程環境中的數據變化。
-
使用
volatile
關鍵字:- 通過在共享變量(如
NextEntryToDo
和EntryCount
)前加上volatile
關鍵字,告訴編譯器這些變量可能會在編譯器無法預見的情況下被其他線程改變。因此,編譯器不會優化掉對這些變量的訪問,并會每次都從內存中加載它們的最新值。 - 這樣做的好處是,即使編譯器認為某些變量不會被修改,它也會確保每次訪問這些變量時,都能獲取到最新的值,從而防止因為編譯優化導致的錯誤。
- 通過在共享變量(如
-
解決問題:
- 通過使用
volatile
,成功消除了之前的待辦事項,使得NextEntryToDo
和EntryCount
這兩個變量的變化能夠被正確處理,而不需要編譯器提前做出假設或優化。
- 通過使用
-
總結:
- 使用
volatile
關鍵字后,編譯器能夠正確識別和處理多線程中變量的變化,確保每次訪問這些變量時都能獲取到最新的值,從而避免了并發修改問題。
- 使用
待辦事項 4:讀取順序
在這段討論中,主要講解了如何確保多線程環境中讀取操作的順序性,避免由于編譯器優化導致的潛在問題。
-
讀取順序問題:
- 討論中提到,如果編譯器在沒有正確檢查
EntryCount
的情況下提前加載了數據,可能會導致不正確的讀取順序。雖然這種情況看起來比較極端,但它揭示了一個潛在的風險,即編譯器可能會優化掉某些指令的順序,造成線程間的數據競爭和錯誤。 - 然而,作者認為這種情況實際上不太可能發生,或者說,它不是當前代碼中的真正問題。盡管如此,理解編譯器可能出現的極端優化情況仍然很重要。
- 討論中提到,如果編譯器在沒有正確檢查
-
InterlockedIncrement
的作用:InterlockedIncrement
函數本身就像一個處理器級的內存屏障(fence),它保證了操作的原子性,并確保內存中的讀寫順序不會被破壞。因此,它已經起到了防止競爭條件的作用,確保在執行遞增操作時不會出現線程之間的沖突。- 即使如此,為了確保更高的安全性,仍然可以在讀取數據之前加入額外的屏障(如
memory fence
),確保所有的內存操作都已經完成,但實際上,這一步可能是冗余的,因為InterlockedIncrement
已經做了足夠的保證。
-
總結:
- 為了確保多線程環境中的數據讀取順序不被打亂,可以依靠
InterlockedIncrement
來保證內存訪問的順序和原子性。在大多數情況下,額外的內存屏障可能是不必要的,因為InterlockedIncrement
已經起到了相應的保護作用。
- 為了確保多線程環境中的數據讀取順序不被打亂,可以依靠
檢查我們的工作
運行程序后的行為和接下來的一些觀察和改進。
-
程序行為驗證:
- 在運行程序后,觀察到線程按預期正確執行,輸出了從
0
到14
的序列,表示問題已經得到修復。之前的問題可能是由于多線程并發執行時出現了錯誤的操作順序,現在已經通過修復確保了順序正確。
- 在運行程序后,觀察到線程按預期正確執行,輸出了從
-
線程數增加后的測試:
- 將線程數增加到
20
后,程序仍然能夠正常運行,輸出的序列從0
到14
,表示多個線程并發時,程序的行為仍然是正確的,說明修復后的程序在更高的并發情況下表現得更好。
- 將線程數增加到
-
整體改進:
- 整體來看,程序的行為已經得到改進,特別是在多線程并發執行時,數據訪問和順序問題得到有效控制,表明程序的線程安全性有了顯著提升。
-
后續的討論:
- 接下來,還會討論一些與程序性能和行為相關的細節問題,這些問題涉及到如何優化程序的線程處理,以便在更高的負載下仍然能夠保持穩定和高效的運行。
任務完成
在渲染部分,當前的問題是雖然我們知道有多少個任務已經開始,但我們并不知道有多少任務已經完成。這是一個問題,因為如果不知道任務完成的數量,就無法確定渲染何時完成,也無法知道何時可以將位圖返回并顯示到屏幕上。
為了能解決這個問題,除了需要知道有多少個任務已經開始,還需要知道有多少個任務已經完成。解決方法有很多,但為了簡化問題,這里使用最簡單的方法。
有兩種不同的需求:一種是需要知道任務完成的數量,另一種是需要知道具體哪些任務已經完成。這兩者是不同的。如果只需要知道有多少任務完成了,解決方案相對簡單。
具體的做法是,每當一個任務完成時,就對一個記錄完成數量的變量(比如EntryCompletionCount
)執行一次原子遞增操作。這樣,當EntryCompletionCount
等于entry_count
時,就表示所有任務都已經完成。
這個方法實現起來非常簡單,實際上我們可以在每個任務完成時增加一個計數值,直到這個計數值達到任務總數(entry_count
)。當這個條件滿足時,我們就可以確定所有任務已經完成,從而可以繼續進行后續的處理。
最終,可以通過檢查EntryCompletionCount
是否等于entry_count
來決定是否可以進行下一步操作,確保所有任務在執行后續操作前都已經完成。這種方式為后續的操作提供了可靠的同步機制。
等待所有線程完成
我們可以使用一個簡單的循環,比如 while (EntryCount != EntryCompletionCount);
來等待,直到所有任務都完成。這種做法會造成一個自旋鎖,持續檢查任務的完成狀態,直到所有任務都完成。這樣一來,我們就能確保所有任務都執行完之后再進行后續的操作,避免任何任務遺漏或者早期執行。
然而,這種做法也有一些缺點。雖然它能確保任務的正確執行順序,但它會使處理器一直處于忙碌狀態,浪費了處理器的時間和電力。尤其是在多核處理器和現代操作系統中,如果線程一直在循環檢查任務狀態,它就不會讓處理器進入低功耗狀態,導致資源浪費。
在早期的處理器中,這種做法或許沒有問題,因為那時的處理器沒有低功耗模式,不存在需要節省電力的需求。但現在,處理器有多種低功耗狀態,且操作系統支持多任務處理,如果我們一直占用 CPU 資源進行無用的檢查,會對系統的多任務處理能力造成負面影響,導致其他進程無法獲得足夠的 CPU 時間。
為了避免這種情況,我們可以采取一種更智能的方式,即讓線程進入休眠狀態,而不是一直忙等。只有當真正有任務需要處理時,才喚醒這些線程執行工作,這樣可以顯著減少 CPU 資源的浪費,同時也能提高系統的效率。
因此,我們需要重新考慮如何設計線程的等待機制,避免不必要的處理器占用,并確保在需要時才讓線程喚醒執行任務。這樣不僅能減少不必要的資源消耗,還能更好地適應現代操作系統的多任務處理。
掛起和恢復線程
在沒有任務需要處理的情況下,我們需要設計一種機制來讓線程“休眠”或者暫停工作,避免浪費 CPU 資源。當線程沒有任務需要處理時,可以通過某種方式告訴操作系統,當前線程不需要執行工作,操作系統可以將其掛起,釋放 CPU 資源給其他任務。這樣做的目的是避免不必要的計算,降低對處理器的負擔,提升系統的效率。
然而,線程“休眠”并不是指簡單地讓線程進入一種傳統意義上的睡眠狀態。我們實際上是告訴操作系統的調度器,當前線程已經完成了任務,可以暫時掛起,等待重新喚醒。為了實現這一點,我們需要設計一種機制,確保在沒有工作時將線程掛起,并在有新的任務或工作需要時重新喚醒這些線程。
具體來說,當隊列中沒有任務時,我們需要通過某種方式將線程掛起。如果我們能夠成功地將線程掛起,那么在后續的操作中,如果有新任務到來時,我們需要能夠喚醒這些線程,讓它們繼續處理工作。為了做到這一點,我們需要提供一個機制來喚醒這些線程。
這個過程涉及到一些細節,主要是因為這是一種時間敏感的操作。舉個例子,假設線程準備進入休眠狀態,而同時另一個線程正在嘗試喚醒它。如果喚醒的操作發生在線程還未進入休眠狀態之前,那么喚醒操作就可能會失效,導致線程無法被喚醒,進而陷入一個死鎖的狀態。所以,設計這樣的機制時需要小心處理不同線程之間的時序問題,以確保休眠和喚醒操作能夠按預期順利進行,避免線程長時間處于休眠狀態而無法恢復。
因此,除了需要設計線程休眠和喚醒機制外,還需要確保系統在這些操作之間保持良好的時序同步,避免出現線程錯過喚醒信號的情況。
信號量
我們將從一個叫做信號量(semaphore)的原語開始,Windows 提供了這些信號量對象。如果你愿意了解更多,可以自行查閱相關文檔,盡管這里有很多文本需要閱讀,但大致上來說,信號量是一種用于等待的原語。它不僅可以幫助你實現超時操作,還能用于讓線程“休眠”和“喚醒”。信號量會跟蹤一個計數,幫助避免之前提到的時間同步問題,例如某個線程在不適當的時候被喚醒或者永遠處于休眠狀態。
信號量的實現比單純使用全局信號更加可靠,因為它能保證在特定的條件下控制線程的喚醒與休眠,從而避免時序上的問題。現在,我打算介紹如何使用信號量對象,這里有個函數 CreateSemaphore
,它創建了一個信號量。
在創建信號量時,可以設置它的初始計數和最大計數。通常情況下,最大計數會設置為可用線程的最大數目,而初始計數則設置為啟動時已經喚醒的線程數。信號量的目的是控制同時有多少個線程處于活躍狀態,因此可以根據線程的數量來設置信號量的計數。
一旦創建了信號量,我們將獲得一個句柄,這個句柄可以用來與 Windows 提供的各種同步功能一起使用。雖然 Windows 提供了大量的同步原語和函數,例如互斥鎖、條件變量等,但這里我只打算從最基本的信號量開始講解。
CreateSemaphoreExA
是 Windows API 中用于創建信號量(semaphore)對象的函數。這個函數的作用是初始化一個信號量,信號量是一種同步原語,用于控制對共享資源的訪問,通常用于實現生產者-消費者問題或線程間的協作。
函數原型
HANDLE CreateSemaphoreExA(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 信號量的安全屬性LONG lInitialCount, // 信號量的初始計數LONG lMaximumCount, // 信號量的最大計數LPCSTR lpName, // 信號量的名稱DWORD dwFlags, // 額外的標志,控制信號量的行為DWORD dwDesiredAccess // 訪問權限標志
);
參數說明
-
lpSemaphoreAttributes:
- 指向
SECURITY_ATTRIBUTES
結構的指針,它定義了信號量的安全描述符。通常,如果不需要設置特別的安全屬性,可以傳遞NULL
。
- 指向
-
lInitialCount:
- 信號量的初始計數,表示信號量在創建時可用的資源數量。可以理解為信號量的“初始值”,它定義了多少個線程可以同時訪問受保護的資源。
-
lMaximumCount:
- 信號量的最大計數,表示信號量的最大資源數量,系統可以處理的最大線程數。如果達到最大計數,調用
ReleaseSemaphore
時就無法再增加信號量計數。
- 信號量的最大計數,表示信號量的最大資源數量,系統可以處理的最大線程數。如果達到最大計數,調用
-
lpName:
- 指向信號量名稱的指針。如果要創建一個命名信號量,則提供名稱字符串。如果不需要命名的信號量,可以將此參數設置為
NULL
。
- 指向信號量名稱的指針。如果要創建一個命名信號量,則提供名稱字符串。如果不需要命名的信號量,可以將此參數設置為
-
dwFlags:
- 影響信號量創建的標志。通常情況下,該值為
0
,但可以通過此字段控制一些特定行為(例如是否在創建信號量時立即釋放)。
- 影響信號量創建的標志。通常情況下,該值為
-
dwDesiredAccess:
- 用于指定訪問信號量的權限。通常為
0
,表示允許任何訪問方式。
- 用于指定訪問信號量的權限。通常為
返回值
- 如果創建信號量成功,
CreateSemaphoreExA
將返回信號量對象的句柄。 - 如果創建失敗,返回
INVALID_HANDLE_VALUE
,并且可以通過GetLastError
獲取錯誤信息。
使用示例
#include <windows.h>
#include <iostream>int main() {// 創建一個信號量,初始計數為0,最大計數為5HANDLE hSemaphore = CreateSemaphoreExA(NULL, // 使用默認安全屬性0, // 初始計數為05, // 最大計數為5"MySemaphore", // 信號量名稱0, // 沒有額外的標志0 // 默認訪問權限);if (hSemaphore == INVALID_HANDLE_VALUE) {std::cerr << "CreateSemaphoreExA failed with error code " << GetLastError() << std::endl;return 1;}std::cout << "Semaphore created successfully!" << std::endl;// 釋放信號量ReleaseSemaphore(hSemaphore, 1, NULL);// 關閉信號量句柄CloseHandle(hSemaphore);return 0;
}
說明
在上面的示例中,我們創建了一個信號量 MySemaphore
,初始計數為 0,最大計數為 5。這意味著線程調用 ReleaseSemaphore
來增加信號量計數,直到達到 5。如果線程數大于最大計數,其他線程將被阻塞,直到信號量被釋放。
這個示例展示了如何創建一個信號量,釋放它,然后關閉句柄。
總結
CreateSemaphoreExA
是一個創建信號量的函數,允許開發者控制線程的同步。通過適當的信號量計數,可以讓多個線程在某些情況下互斥地訪問共享資源。
c++ 的創建信號量(c++20 才有信號量嗎?)
在 C++ 標準庫中,信號量是一個相對較新的特性,它是在 C++20 標準中引入的。C++20 中引入了 <semaphore>
頭文件,并提供了 std::counting_semaphore
和 std::binary_semaphore
兩種信號量類型。
1. C++20 標準庫信號量
std::counting_semaphore
:允許線程對一定數量的資源進行控制,類似于傳統的信號量,可以控制多個線程訪問有限的資源。std::binary_semaphore
:是一個二進制信號量,其計數值要么是 0,要么是 1,通常用于二進制互斥的場景。
2. std::counting_semaphore
示例
std::counting_semaphore
允許線程獲取和釋放多個資源,其計數可以大于 1。
示例代碼:
#include <iostream>
#include <semaphore>
#include <thread>
#include <vector>std::counting_semaphore<3> sem(3); // 最多允許 3 個線程同時執行void worker(int id) {std::cout << "Worker " << id << " waiting for semaphore...\n";sem.acquire(); // 獲取信號量std::cout << "Worker " << id << " acquired semaphore, working...\n";// 模擬一些工作std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "Worker " << id << " releasing semaphore...\n";sem.release(); // 釋放信號量
}int main() {std::vector<std::thread> threads;// 創建多個線程來測試信號量for (int i = 0; i < 5; ++i) {threads.push_back(std::thread(worker, i));}// 等待所有線程完成for (auto& t : threads) {t.join();}return 0;
}
3. 代碼說明
-
std::counting_semaphore<3> sem(3)
:創建一個信號量,最大計數為 3,表示最多可以有 3 個線程同時訪問資源。初始計數為 3,表示有 3 個資源可用。 -
sem.acquire()
:調用acquire()
來獲取信號量。如果信號量計數為 0,線程將阻塞,直到信號量計數大于 0。 -
sem.release()
:調用release()
來釋放信號量,增加信號量計數。每次調用release()
都會喚醒一個被阻塞的線程(如果有的話)。 -
std::this_thread::sleep_for()
:模擬工作,線程執行時會休眠一段時間,以便模擬實際工作負載。 -
多線程:通過創建多個線程來同時測試信號量,保證最多只有 3 個線程能同時進入工作狀態。
4. std::binary_semaphore
示例
std::binary_semaphore
是一個二進制信號量,計數只能是 0 或 1,通常用于實現互斥鎖。
示例代碼:
#include <iostream>
#include <semaphore>
#include <thread>
#include <vector>std::binary_semaphore sem(1); // 最多允許 1 個線程同時執行,類似于互斥量void worker(int id) {std::cout << "Worker " << id << " waiting for semaphore...\n";sem.acquire(); // 獲取信號量std::cout << "Worker " << id << " acquired semaphore, working...\n";// 模擬一些工作std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "Worker " << id << " releasing semaphore...\n";sem.release(); // 釋放信號量
}int main() {std::vector<std::thread> threads;// 創建多個線程來測試信號量for (int i = 0; i < 5; ++i) {threads.push_back(std::thread(worker, i));}// 等待所有線程完成for (auto& t : threads) {t.join();}return 0;
}
5. 代碼說明
-
std::binary_semaphore sem(1)
:創建一個二進制信號量,初始計數為 1,這意味著最多只有一個線程可以獲取信號量。它的行為類似于互斥量。 -
sem.acquire()
:獲取信號量,若信號量計數為 0,線程將阻塞,直到信號量可用。 -
sem.release()
:釋放信號量,增加信號量計數。調用release()
后,如果有線程在等待信號量,它將被喚醒。
6. 小結
C++20 中引入的信號量 (std::counting_semaphore
和 std::binary_semaphore
) 使得并發編程更加簡潔和安全。std::counting_semaphore
適用于多個資源共享的場景,而 std::binary_semaphore
則更適合實現互斥鎖。通過這兩個信號量,開發者可以輕松地控制并發線程的數量,避免資源競爭和死鎖等問題。
使用 WaitForSingleObject 掛起線程
在多線程編程中,WaitForSingleObject
的概念是指等待一個特定的對象狀態變化,通常是等待某個同步對象(如信號量、事件、互斥量等)被觸發。具體來說,這種等待機制會暫停當前線程的執行,直到指定的對象被“信號化”或滿足某個條件為止。
在這種機制中,我們通常會調用一個函數,它需要一個“句柄”(handle),這是指向一個操作系統資源的指針(例如,信號量的句柄)。此外,這個函數還會接收一個時間參數,用來指定等待的最長時間,通常可以設定為無限等待,也就是一直掛起線程,直到某個條件觸發。
函數行為分析
-
句柄參數:
WaitForSingleObject
函數接受一個句柄,它代表了操作系統中的一個同步對象(例如信號量)。這個對象是操作系統為線程同步提供的資源,通過等待它被信號化來控制線程的執行。 -
時間參數:函數會接收一個時間參數,表示線程等待的最長時間。通常,如果這個時間設置為無限,線程就會一直等待,直到指定的同步對象被觸發。也可以設定一個具體的時間,以便在超時后返回,允許線程進行其他操作。
-
返回值:函數會返回一個狀態值,指示等待的結果。通常,返回值可以告訴我們是因為同步對象被信號化而使線程繼續執行,還是因為超時而中止等待。雖然這部分內容可以被忽略,但在設置了超時的情況下,返回值會提供重要的信息,例如線程是因為同步對象的信號被觸發而繼續執行,還是因為超時導致線程被喚醒。
-
工作原理:當我們調用這個函數時,操作系統會將當前線程掛起,直到同步對象被觸發。線程會進入休眠狀態,釋放其占用的CPU資源,讓操作系統可以調度其他線程。對于設置為無限等待的情況,線程會持續休眠,直到目標同步對象被“信號化”或觸發。
-
多種同步對象支持:這種等待機制的設計非常通用,不僅支持等待信號量,還可以支持等待其他類型的同步對象(如事件、互斥量等)。因此,它是一個非常靈活的機制,可以用于處理多種不同的同步需求。
-
節省資源:等待過程中,線程處于休眠狀態,這意味著它不會占用CPU資源,允許操作系統將CPU時間分配給其他需要運行的線程或任務。在某些情況下,這也能幫助節省電池或系統的功耗,特別是在移動設備或需要長時間運行的程序中。
-
信號化與喚醒:當某個條件滿足時,例如信號量被“信號化”,等待的線程會被喚醒,恢復執行。同步對象的“信號化”機制是多線程編程中的關鍵,控制著線程的啟動、暫停和同步,保證了不同線程之間的正確協調和資源共享。
總結來說,這個等待機制為線程提供了一個高效的掛起與恢復機制,讓線程在等待同步對象時能夠釋放資源、節省功耗,并且在同步對象被觸發時恢復執行,保證了多線程操作的高效和協調。
創建信號量
在多線程編程中,首先我們需要創建一個信號量(semaphore)來同步線程的執行。創建信號量的過程通常涉及調用 CreateSemaphoreEx
或類似的函數,該函數返回一個信號量句柄,這個句柄將用于后續的線程同步操作。
1. 創建信號量
信號量的創建過程中,我們不需要特別的安全屬性,因此可以將其設置為 NULL
。此外,信號量也不一定需要命名,雖然如果為信號量命名,可以通過名字來查詢信號量,但在這種情況下,我們可以直接將名稱設為 NULL
,因為我們已經有了信號量的句柄。
創建信號量時,通常會指定信號量的初始計數和最大計數。初始計數設定了信號量的初始狀態,最大計數則限制了信號量的最大值。這里初始計數可以設置為 0,表示所有線程將被掛起,直到信號量被釋放。
2. 線程數量與信號量的配合
假設我們有多個線程需要等待這個信號量的信號。在創建線程時,每個線程將獲取信號量的句柄,并在執行過程中調用 WaitForSingleObjectEx
等函數來等待信號量的觸發。每個線程將一直等待信號量的信號,直到它被釋放。
3. 線程等待與信號量的作用
在線程的執行過程中,每個線程都會調用 WaitForSingleObjectEx
,并傳入信號量句柄。如果信號量沒有被觸發,線程將會掛起,直到信號量被其他線程釋放。當信號量被釋放時,掛起的線程會被喚醒并繼續執行。
但是,如果所有線程在信號量釋放之前就到達了 WaitForSingleObjectEx
并等待,且沒有提前被信號量喚醒,那么這些線程會進入掛起狀態,什么都不做。因此,這種情況下如果主線程沒有先執行某些操作(例如推送工作到隊列中),這些線程可能什么都不打印,直到信號量被正確信號化。
4. 信號量的初始值與線程行為
在信號量創建時,初始計數設為 0,這意味著線程會在信號量釋放之前被掛起。這樣,線程不會執行任何操作,直到信號量被其他線程觸發。一旦信號量被觸發,所有等待該信號量的線程就會被喚醒,繼續執行。
信號量的這種機制可以有效地管理并發執行,確保只有在特定條件下線程才會繼續執行。在本例中,線程會等待信號量的信號,并且只有在信號量被觸發后,它們才會繼續進行后續操作。
5. 線程競爭和輸出
在這種多線程環境下,主線程和工作線程之間可能存在競爭條件。主線程在執行時可能會提前向隊列中添加任務,或是觸發信號量。而其他線程可能在主線程進行這些操作之前就已經到達了 WaitForSingleObjectEx
,因此它們將會處于等待狀態。如果信號量沒有被及時釋放,線程將一直掛起,直到被喚醒。
如果有線程提前被喚醒,它們可能會輸出一些調試信息,具體輸出內容取決于線程的執行順序和信號量的觸發時機。如果線程在信號量觸發之前沒有機會運行,那么它們就不會執行任何打印操作。
總結
- 創建信號量時,可以設置初始值為 0,這樣所有線程會被掛起,直到信號量被釋放。
- 線程通過
WaitForSingleObjectEx
等函數來等待信號量的觸發,這樣可以確保線程的執行順序和同步。 - 通過信號量機制,線程可以在正確的時機進行工作,避免了資源的競爭和沖突。
- 在多線程程序中,線程的執行順序和信號量的觸發時機可能影響最終的輸出結果,尤其是在競爭條件下,輸出可能會有所不同。
這種多線程和信號量的配合模式非常適合用于處理需要精確同步的任務,確保線程按預定順序或條件執行。
問題:線程從未醒來!
在多線程編程中,理解線程的執行順序和同步機制非常重要。在這個過程中,創建多個線程并通過信號量或其他同步機制來控制線程的執行是一個關鍵點。以下是對上述內容的詳細總結:
線程創建與執行順序
在這個例子中,我們創建了多個線程(具體是 15 個線程)。在創建這些線程的過程中,主線程的執行并沒有立即開始將字符串推入隊列。主線程在創建每個線程時,都會執行一些初始化操作,包括創建線程并等待它們開始執行。
-
線程啟動:線程的創建是并行進行的,每個線程都會進入它的啟動例程(start routine)。然而,線程在執行的過程中會遇到一個“等待”操作,即它們會進入
WaitForSingleObjectEx
等待信號量或其他同步信號。此時,所有的線程都被掛起,直到它們收到信號量的信號。 -
線程競爭:由于主線程在創建所有線程后才開始推送字符串到隊列,因此它在推送字符串之前需要確保所有線程已經被創建并進入等待狀態。在這個過程中,主線程推送字符串的時機變得至關重要。
線程執行與等待
在所有線程創建完畢后,它們會進入各自的等待狀態。特別是,線程在調用 WaitForSingleObjectEx
后會掛起,等待信號量的信號。而隊列中的字符串推送操作則會由主線程控制。
-
線程 3和4 的特殊情況:由于主線程推送字符串的時機很關鍵,線程 3&4 成為了一個特殊的情況。線程 3&4 是最后一個被創建的線程,它在其他線程已經開始等待時,成功地到達了隊列并執行了推送操作。因此,它能夠在其他線程之前執行完任務并開始處理。這個過程會導致線程 3&4 能夠比其他線程更早完成任務。
-
輸出順序:由于每個線程都在等待信號量并按順序執行,輸出的字符串會按照預期的順序打印。因為只有一個線程在處理隊列中的任務,其他線程都在等待,最終的輸出順序是確定的。
線程調度與等待的偶然性
如果所有線程在執行時都不進入等待狀態,而是直接進行工作,它們就會在沒有信號量的保護下并行執行,導致無法按預期順序處理隊列中的任務。這個情況是偶然的,因為它依賴于線程啟動的順序以及隊列的處理情況。
- 線程延遲:如果線程在執行前有一定的等待時間,它們就有機會進行處理并執行工作。如果所有線程都等待并按順序執行,那么隊列中的任務就能夠正常按順序被處理。
信號量的作用
為了確保線程按預期順序執行,并且避免引發競爭條件和錯誤,信號量的使用是必須的。信號量能夠控制線程的喚醒時機,確保線程按預定的順序執行。
- 信號量的使用:信號量用于控制線程的掛起和喚醒,確保線程在正確的時機被喚醒。通過信號量,線程可以在特定時刻被喚醒,避免線程在沒有資源的情況下繼續執行。
解決方案與改進
為了避免上述偶然性帶來的問題,應該引入更精確的同步機制,確保線程能夠按預定的順序執行,而不會在執行過程中出現不可預測的情況。通過使用信號量或其他同步對象,可以確保線程在正確的時機開始工作,從而避免資源競爭和程序錯誤。
總之,線程的創建、同步和執行順序是多線程編程中的核心內容,理解并正確使用信號量、互斥量等同步機制,可以幫助我們編寫出更穩定和高效的多線程程序。
通過釋放信號量喚醒線程
在這段討論中,主要涉及到信號量(semaphore)的創建和使用。首先,通過 CreateSemaphoreExA
創建信號量,并設置初始計數為 0。初始計數為 0 意味著一開始沒有任何線程會被喚醒,線程需要等待信號量計數增加才能繼續執行。
-
信號量的創建與作用:
創建信號量時,信號量的初始計數被設置為 0,這意味著線程一開始無法獲取信號量,因此會進入等待狀態。信號量的作用是控制并發線程的執行,確保在某些條件下才允許線程執行,避免競態條件或資源爭用。 -
ReleaseSemaphore
的使用:
每當有新的工作項被添加到隊列中時,會調用ReleaseSemaphore
來增加信號量的計數,從而喚醒等待的線程。例如,當一個字符串被推送到隊列時,調用ReleaseSemaphore
,此時信號量的計數會增加,可能會喚醒一個等待中的線程,讓它繼續處理工作。 -
信號量計數的遞增與遞減:
信號量計數的增加是通過ReleaseSemaphore
來實現的,每當執行ReleaseSemaphore
時,信號量的計數會增加。這表明有新的工作項可以被處理,等待的線程可以開始執行。而當線程通過WaitForSingleObject
等函數等待信號量時,信號量的計數會被遞減,因為線程會“消耗”掉一個信號量,表示它已經處理了一個工作項。 -
線程的喚醒與等待:
每個線程在開始執行前會調用WaitForSingleObject
來等待信號量的釋放。當信號量計數大于 0 時,線程可以獲得信號量并開始執行。如果信號量的計數為 0,線程會一直阻塞,直到信號量計數增加為止。 -
信號量的同步:
信號量的計數決定了有多少個線程可以同時執行。通過控制信號量的計數值,可以精確地控制線程的同步。例如,多個線程可以同時訪問一個共享資源,或者多個線程可以依次執行某些任務,確保不會出現資源爭用或競態條件。 -
潛在的同步問題:
如果信號量的計數沒有正確管理,可能會導致線程無法被正確喚醒或導致線程進入死鎖狀態。例如,如果線程在獲取信號量之前沒有正確更新信號量計數,可能會導致線程永遠處于等待狀態。 -
WaitForSingleObject
與信號量計數:
WaitForSingleObject
函數本身不會改變信號量的計數值。它只是檢查信號量的狀態并使線程進入等待狀態。信號量的計數值僅在信號量被釋放(通過ReleaseSemaphore
)時才會增加。當一個線程成功獲得信號量后,信號量的計數會減少,表示該線程已開始處理工作項。 -
信號量的正常工作流程:
- 當一個工作項被加入隊列時,信號量計數會通過
ReleaseSemaphore
增加,喚醒一個等待的線程。 - 線程在等待信號量時,通過
WaitForSingleObject
阻塞自己,直到信號量的計數大于 0。 - 每個線程處理完一個任務后,會再次減少信號量計數,表示工作已經完成,其他線程可以繼續執行。
- 當一個工作項被加入隊列時,信號量計數會通過
總結起來,信號量的工作機制通過管理計數值來確保線程同步。當有新任務時,信號量計數增加,喚醒線程進行處理;當線程完成任務時,信號量計數減少,表示資源已經被釋放。通過這種機制,可以確保線程之間的同步與資源的安全訪問。
我們將如何使用信號量
在這段描述中,討論了如何使用信號量來控制線程的睡眠狀態。信號量通常用于指示隊列中的工作量,但在這個例子中,并不直接用信號量來跟蹤工作量,而是將它作為一個通用的機制來控制線程何時進入睡眠狀態。
具體來說,信號量的計數并不會直接與工作量掛鉤,而是僅用于確保在任意時刻,線程數量不會超過最大線程數。信號量的作用是暫停線程的執行,直到某些條件觸發,例如某個線程完成了它的任務。
接下來,程序運行時,應該能夠看到多個線程并行工作,處理不同的任務,這個過程會產生不同的輸出。例如,在運行中,線程可能會依次輸出類似 “0 1 2 3”、“1 2 3”、“4 5 6” 等結果,表示這些線程正在不同的時間點被喚醒并執行任務。
此外,程序還展示了有無信號量等待(wait
)機制時的不同表現。如果不使用等待機制,線程可能無法按預期工作,從性能上看,程序的表現也會有所不同。在沒有等待的情況下,線程沒有進入睡眠狀態,可能會出現線程資源的浪費或性能下降的現象。
測試信號量
在這段描述中,討論了如何改進線程的管理,避免不必要的 CPU 占用和浪費,從而提高程序的效率和系統的多任務處理能力。
最初,如果線程沒有被正確地控制,它們會處于一個“自旋”狀態,即不斷地檢查某個條件,但由于沒有工作要做,線程會反復無意義地進行檢查。這種情況下,處理器的負載會飆升到 100%,因為許多 CPU 核心會被這些空轉的線程占用,導致資源浪費,甚至對電池續航造成影響。這顯然是低效的。
為了解決這個問題,采用了信號量和 WaitForSingleObject
來控制線程的行為。通過這種方式,線程只有在真正有工作需要執行時才會被喚醒,從而避免了之前的資源浪費。在優化后,程序的 CPU 使用率降低到 6%,這正是預期的效果,表明只在有工作的時候才使用處理器。
接著,還進行了另一個測試。測試的目的是確保線程在長時間休眠后能夠正確地醒來并繼續執行任務。通過模擬一個 5000 毫秒的休眠時間,確保所有線程都進入了睡眠狀態。之后,當有新的工作(字符串)加入隊列時,所有線程都能被正確地喚醒并處理這些任務。測試結果顯示,線程能夠按預期工作,輸出正確的結果,表明線程管理已經達到了預期目標。
最終,通過這種方法,成功創建了一個工作隊列,確保線程能夠有效地處理任務,并在沒有工作時將 CPU 時間讓給操作系統,避免了不必要的資源浪費和競爭條件,從而提高了系統的整體效率。
在 Visual Studio 輸出窗口中,你可以右鍵點擊并取消選擇一些內容
在這段對話中,討論了 Visual Studio 中輸出窗口的一些實用技巧。具體來說,提到了如何通過右鍵點擊輸出窗口來選擇某些選項,這樣可以幫助開發者更加方便地查看程序輸出。雖然之前沒有嘗試過這種方法,但發現它非常實用,并且感覺這個功能非常棒。最后,表示希望能看到程序的輸出結果,認為這對于調試和查看程序運行情況非常有幫助。
為什么這必須這么復雜??
討論了編程中使用隊列的復雜性。對于一些人來說,隊列的使用可能看起來有些復雜,但實際上并沒有那么難理解。關鍵在于,它更容易出錯,而不是非常復雜。一旦掌握了如何使用隊列,通常可以
順利地使用它。只要小心處理,問題不會很大。問題出現在需要處理復雜的互鎖操作時,這類操作容易引發錯誤,因此最好盡量避免做過于復雜的互鎖操作。
為什么你把內存屏障放在一個宏中,而這段代碼是平臺特定的?
雖然這段代碼在某些平臺上可能是特定的,但實際上,內存屏障的實現可能并不總是和平臺綁定。最終,這些實現將被移到渲染部分,變成與平臺無關的代碼。因此,需要為每個平臺在頭文件中定義這些內存屏障,并根據不同的平臺使用它們。這種做法類似于其他平臺特定的內置函數。之后,一些線程會在經過一段睡眠后推送多個字符串到隊列中。
在 sleep 之后,仍然有一些線程推送了多個字符串,遺漏了一些其他線程
首先,操作系統會根據信號量的狀態決定何時喚醒線程。線程是否能夠處理隊列中的任務,取決于操作系統的調度程序如何分配資源。通常,最先被喚醒的線程會處理隊列中的大部分任務,因為它會先獲取任務并開始處理。而在某些情況下,當其他線程喚醒時,可能發現隊列中已經沒有任務可處理,這就導致它們無法執行任何工作。因此,線程是否能成功處理任務是由操作系統調度的復雜性和隨機性決定的。
在你的自旋鎖中使用 Sleep(0) 會有幫助嗎?
自旋鎖通常是在等待某個條件時不斷檢查某個標志位或信號量狀態的,而使用 sleep(0)
可能不會有效地減少 CPU 的占用,因為它依舊會導致線程不斷循環檢查。
重點是,當所有的線程都進入睡眠狀態時,信號量的值應該會變為零。因此,在信號量變為零時,應該能夠通過等待該信號量的方式來避免繼續自旋等待。這樣,線程就不需要持續占用 CPU 資源,而是可以更高效地等待信號量的變化。換句話說,應該采用一種更高效的等待機制,而不是通過自旋鎖不斷檢查。
總結來說,使用 sleep(0)
在自旋鎖中并不是最理想的做法,可以通過更合理的等待機制來優化線程的等待和信號量的處理,從而避免浪費 CPU 資源,確保線程的高效管理。
我錯過了今晚的大部分內容,volatile 關鍵字是什么意思?
volatile
關鍵字用于告訴編譯器不要優化特定變量的讀取操作。它的作用是確保每次都從內存中讀取該變量的值,而不是使用寄存器中緩存的值。這是因為這個變量可能會被其他線程修改,因此它的值不應當被緩存或優化掉。使用 volatile
關鍵字可以保證程序每次訪問該變量時都能夠獲得最新的值,而不是讀取到被優化掉的舊值。
你打算如何維護處理器之間的緩存行一致性?物理 CPU 能共享一個緩存行嗎?
關于如何在處理器之間保持緩存行的一致性,實際上已經不再使用傳統的系統總線鎖了。現在的技術使用的是更現代的機制,例如 MESI(修改、獨占、共享、無效)協議。這個協議幫助確保多個處理器之間的緩存一致性,不需要使用傳統的鎖機制來協調緩存數據。
傳統上,處理器間的數據共享和同步是通過鎖來保證的,然而隨著多核處理器和現代計算架構的發展,像 MESI 這樣的緩存一致性協議能夠通過協調緩存狀態,避免了傳統鎖的使用。因此,現代處理器可以通過這些協議自動管理數據一致性,避免了性能瓶頸。
對于“無鎖編程”(lock-free programming),也正是因為這種緩存一致性協議的存在,它實際上變得更可行。不同處理器的緩存可以在不通過鎖的情況下進行有效同步和協調,這使得并發操作能夠更高效。
盡管如此,了解這些機制和協議需要深入的硬件和系統架構知識,而這部分內容通常不容易掌握,因為涉及到底層硬件的工作原理。
黑板:MESI 和緩存一致性
在現代處理器架構中,多個處理器和核心之間保持緩存一致性是一個非常重要的任務。這個任務通常由處理器芯片組(如Intel芯片組)來自動管理。為了保證緩存的一致性,芯片組采用了名為MESI協議(Modified, Exclusive, Shared, Invalid)的機制。該協議通過緩存行的狀態標志和“窺探”技術(snooping)來確保不同核心對共享內存的訪問不會發生沖突。
MESI協議
MESI協議的核心思想是通過對緩存行進行標記,來管理緩存之間的數據同步。每個緩存行有四種狀態:
- Modified(修改狀態):該緩存行的內容已被修改,并且是唯一的副本。此時,緩存中的數據不同于主存中的數據。
- Exclusive(獨占狀態):該緩存行只存在于當前緩存中,且與主存中的數據一致。
- Shared(共享狀態):該緩存行在多個核心中都存在,并且所有副本都與主存中的數據一致。
- Invalid(無效狀態):該緩存行的數據已經無效,需要重新從主存或其他緩存獲取數據。
緩存一致性的維護過程
當一個核心想要加載一個緩存行時,它會檢查該緩存行的狀態。如果緩存行已經被其他核心修改過,當前核心的緩存行會被標記為無效(Invalid),并且需要從其他核心或主存中獲取最新數據。這個過程稱為“窺探”(snooping)。
在多個核心同時訪問相同內存地址的情況下,MESI協議確保:
- 當兩個核心都讀取同一緩存行時,緩存行會被標記為“共享”狀態。
- 如果其中一個核心修改了該緩存行,它會將該緩存行標記為“修改”狀態,并通知其他核心將該緩存行標記為“無效”,確保其他核心不再使用過時的數據。
為什么處理器自動管理緩存一致性
現代處理器的芯片組通過硬件機制來自動保證緩存的一致性,程序員無需手動干預。芯片組會負責檢測和處理緩存行的狀態變化,保證不同核心之間的數據一致性。因此,程序員只需要關注如何正確編寫代碼,確保在多個核心間的共享數據訪問不會發生沖突。
注意事項
盡管芯片組能夠處理大部分緩存一致性問題,但程序員仍然需要關注特定的細節,特別是在涉及到寄存器和內存之間的交互時。例如,當程序對某個寄存器進行操作,并期望這些操作反映到內存中時,處理器并不能自動確保所有核心能看到這些更新。因此,程序員需要使用合適的同步機制,如互斥鎖、原子操作等,確保在進行“讀取-修改-寫入”操作時,數據的一致性和正確性。
總結
在現代多核處理器中,緩存一致性主要由處理器的芯片組自動管理,通過MESI協議和緩存“窺探”機制來保證多個核心之間的數據同步。程序員無需關注這些底層的細節,只需確保合理使用同步機制來處理寄存器和內存的交互,以避免數據競爭和不一致的問題。
volatile 是在 C99 中添加的嗎?
volatile
關鍵字在 C 語言中的引入時間較為久遠,通常被認為是在 C 語言標準較早的版本中就已經存在。這個關鍵字的作用是告訴編譯器,不要對標記為 volatile
的變量進行優化。也就是說,每次訪問這個變量時,都必須從內存中讀取,而不能使用緩存中的值。這對于涉及硬件寄存器、并發編程或者操作外部設備等場景非常重要,因為在這些場景中,變量的值可能會在程序不執行任何操作的情況下發生變化,而編譯器通常會進行優化,假設變量值不會發生變化,從而可能導致程序錯誤。
至于 volatile
是否在 C99 標準之前已經存在,很多人認為它早在 C 語言的標準化初期就已經引入,而不是在 C99 版本才出現。這意味著,即使在 C99 標準之前,volatile
也已經是 C 語言中的一部分,用于確保在特定情況下對內存中的變量進行可靠訪問,避免編譯器的優化行為影響程序的正確執行。
事務性內存通過允許一組加載/存儲指令原子執行來簡化并發編程。你有試過這個嗎?
事務性內存(Transactional Memory,簡稱 TM)旨在簡化并發編程,通過允許一段加載和存儲指令在原子操作中執行,從而減少并發編程中的復雜性。事務性內存的核心思想是將一組指令作為一個“事務”進行處理,在事務內部的所有操作要么全部成功執行,要么完全不執行,這樣可以避免傳統鎖機制帶來的復雜性和性能問題。
不過,事務性內存的實現并不總是順利的。當前有些處理器或平臺支持事務性內存,但它們的實現可能并不完美,甚至有些可能存在bug或性能問題。因為事務性內存的復雜性,在一些桌面處理器上可能會被禁用,或者只是作為實驗性功能存在,而并不是所有硬件都穩定支持這一技術。
因此,盡管事務性內存理論上提供了一種簡化并發編程的方式,但在實際使用中,可能仍然會遇到一些問題或限制,特別是在硬件和驅動的支持上。
黑板:事務性內存
事務性內存(Transactional Memory)是一種旨在簡化并發編程的技術,主要通過減少鎖的使用來提升并發效率。它的核心概念是“鎖消除”(Lock Elision),即在某些情況下可以避免使用鎖,而是通過一個原子操作來確保多個線程訪問共享內存時的一致性。
在事務性內存的機制中,程序會將一系列的內存操作包裝成一個“事務”。事務開始時,程序標記該區域為“開始事務”,并開始執行對內存的修改。事務結束時,程序會檢查在此期間是否有其他線程修改了相同的內存區域。如果沒有其他線程修改數據,事務中的所有更改將被“提交”,否則將不會提交這些修改。
具體來說,事務性內存的工作原理類似于一個大型的互鎖比較交換(Compare-and-Swap)操作。程序通過事務性內存來確保在修改一塊內存區域時,期間沒有其他線程寫入過該內存區域。這樣,如果沒有沖突,所有修改將被提交;如果存在沖突,則程序會回滾這些修改,轉而使用傳統的鎖機制來保護數據的更新。
事務性內存的優點在于,它能在沒有顯式鎖的情況下執行多線程任務,從而提升性能和簡化代碼。然而,事務性內存的實現依賴于硬件支持,且并不是所有處理器都穩定支持該功能。有些處理器可能在硬件上存在問題,導致該功能被禁用,或在使用過程中出現不穩定的情況。因此,事務性內存的普及和穩定性仍然是一個挑戰。
簡而言之,事務性內存通過模擬一個單線程環境來避免顯式鎖的使用,并在沒有沖突的情況下提交事務。這使得多線程編程更加簡潔和高效,但具體實現和使用時依然需要注意硬件的支持情況。
事務性內存常被宣傳為一種更易使用的替代鎖的方法,避免任何死鎖的可能性,我想聽聽你的想法。
事務性內存(Transactional Memory)常被提倡作為替代傳統鎖的簡化方案,主要的優點是能夠避免死鎖問題。事務性內存在理論上比傳統的鎖機制更高效,尤其是在某些極限條件下。然而,實際應用中并不是所有場景都需要使用鎖。特別是在游戲開發中,有很多情況下根本不需要使用真正的鎖。
因此,首先應該盡量避免使用鎖,而是通過設計盡量不依賴鎖的代碼。如果確實遇到必須使用事務性內存的情況,再去考慮它。對于一些需要并發的操作,事務性內存提供了一種較為簡便的方式來管理并發訪問,但也并不總是必需的。
需要注意的是,事務性內存本身是一個概念,而不是硬件功能。雖然現代處理器(如x86架構)提供了實現事務性內存的功能,但實際上,可以不依賴硬件支持,而通過程序邏輯來模擬事務性內存的操作。比如,在某些模擬程序中,可以在將計算結果提交到主存之前,檢查是否有其他線程修改了相同的數據。如果發現沖突,可以中止當前操作,這種方式就類似于事務性內存的概念。
總的來說,事務性內存雖然在某些情況下提供了簡化并發管理的方法,但并不是所有情況都需要依賴它。如果能夠通過無鎖編程或者其他方法避免鎖,盡量不使用鎖是更為理想的方案。只有在確實需要時,才考慮采用事務性內存的概念來處理并發操作。
為什么我們要構建一個通用的工作分發系統,而平鋪渲染器本身就已經能夠很干凈地分割工作了?
我們構建一個通用的工作分發系統,是因為盡管平鋪渲染已經設計好了將工作分割成多個小塊(tiles),但是每個小塊的處理時間是不確定的。有些小塊可能需要很長時間來處理,而有些可能只需要極短的時間。這是因為一些小塊可能涉及復雜的粒子系統,而其他小塊可能只是簡單的單棵樹的渲染。
因此,我們需要將所有的小塊放入一個工作隊列中,然后讓多個線程從隊列中取出任務進行處理。這樣,如果某個線程不幸取到了一個需要較長時間處理的任務(比如一個耗時較長的小塊),它不會阻塞其他線程的工作,其他線程仍然可以繼續處理剩下的小塊,直到耗時較長的小塊處理完成。這種方式保證了即使某個小塊的處理時間較長,整體渲染也不會因為等待某個任務的完成而拖慢。
盡管我們已經將工作分割成了多個小塊,但依然需要一個工作隊列來管理這些任務。我們不需要非常復雜的工作隊列系統,因此并沒有設計一個特別復雜的隊列,但我們確實需要一個工作隊列來有效分配任務,這就是我們所構建的隊列系統。
總結
我們已經完成了多線程的基礎部分,包括如何構建一個工作隊列,基本上這些內容已經做完。接下來,我們需要做的就是利用這個工作隊列來調用渲染函數,這將是明天的任務。
不過,還有一件事情需要處理:我們要展示如何讓主線程避免忙等待。雖然我們也可以選擇在一開始不處理,等到后面再做,但為了完整性,還是應該在早期就處理掉。完成這個之后,我們會開始實際調用渲染函數,這涉及一些跨平臺的代碼優化工作。
除此之外,我們還需要清理渲染部分的一些問題,因為這些問題目前導致我們無法在多線程環境下運行。希望這些問題能夠在調試過程中被發現,大家也能看到實際問題的存在,幫助更好地理解。