UE4原子操作與無鎖編程


原子操作的Interlocked函數

// 自增操作
int32 n1 = 100;
// n1=n1+1; r1=n1; 
int32 r1 = FPlatformAtomics::InterlockedIncrement(&n1); // n1=101  r1=101

// 自減操作
int32 n2 = 110;
// n2=n2-1; r2=n2;
int32 r2 = FPlatformAtomics::InterlockedDecrement(&n2); // n2=109  r2=109

// Fetch and Add操作
int32 n3 = 120;
// r3=n3; n3=n3+5;
int32 r3 = FPlatformAtomics::InterlockedAdd(&n3, 5);  // r3=120  n3=125

// 數值Swap操作
int32 n4 = 130;
// r4=n4; n4=8;
int32 r4 = FPlatformAtomics::InterlockedExchange(&n4, 8); // r4=130  n4=8

// 指針Swap操作
int32 n51 = 140;
int32 n52 = 141;
int32* pn51 = &n51;
int32* pn52 = &n52;
// r5=pn51;  pn51=pn52;
void* r5 = FPlatformAtomics::InterlockedExchangePtr((void**)&pn51, pn52);  // *r5=140  *pn51=141  *pn52=141

// 數值Compare and Swap操作
int32 n61 = 150;
int32 n62 = 151;
int32 n63 = 150;
// r6=n61;  if (n61==n63) {n61=n62;}
int32 r6 = FPlatformAtomics::InterlockedCompareExchange(&n61, n62, n63);  // r6=150  n61=151  n62=151

int32 n71 = 160;
int32 n72 = 161;
int32 n73 = 60;
// r7=n71;  if (n71==n73) {n71=n72;}
int32 r7 = FPlatformAtomics::InterlockedCompareExchange(&n71, n72, n73);  // r7=160  n71=160  n72=161

// 指針Compare and Swap操作
int32 n611 = 150;
int32 n622 = 151;
int32 n633 = 150;
int32* pn611 = &n611;
int32* pn622 = &n622;
int32* pn633 = &n633;
// r6x=pn611;  if (pn611==pn633) {pn611=pn622;}    注:比較的是指針地址
void* r6x = FPlatformAtomics::InterlockedCompareExchangePointer((void**)&pn611, pn622, pn633); // r6x=pn611  *pn611=150  *pn622=151

int32 n711 = 160;
int32 n722 = 161;
int32* pn711 = &n711;
int32* pn722 = &n722;
int32* pn733 = &n711;
// r7x=pn711;  if (pn711==pn733) {pn711=pn722;}    注:比較的是指針地址
void* r7x = FPlatformAtomics::InterlockedCompareExchangePointer((void**)&pn711, pn722, pn733); // r7x=pn711  pn711=pn722  *pn711=161  *pn722=161

// 數值與運算
int32 n81 = 8;
int32 n82 = 12;
int32 r8 = FPlatformAtomics::InterlockedAnd(&n81, n82);  //r8=8  n81=1000&1100=1000=8   n82=12

// 數值或運算
int32 n91 = 9;
int32 n92 = 12;
int32 r9 = FPlatformAtomics::InterlockedOr(&n91, n92);  //r9=9  n91=1001|1100=1101=13   n92=12

// 數值異或運算
int32 na1 = 9;
int32 na2 = 12;
int32 ra = FPlatformAtomics::InterlockedXor(&na1, na2);  // ra=9   na1=1001^1100=0101=5   na2=12

 

FCriticalSection(用戶模式下的臨界區段)

當有線程進入臨界區段時,其他線程必須等待。基於原子操作Interlocked函數實現。

優點:效率高(不需要昂貴的用戶態切換到內核態的上下文切換)

缺點:不能用於進程間同步,只能用於進程內各線程間同步

平台 實現類
Windows typedef FWindowsCriticalSection FCriticalSection;

Mac

Unix

Android

IOS

typedef FPThreadsCriticalSection FCriticalSection;
HoloLens typedef FHoloLensCriticalSection FCriticalSection;

 

FSystemWideCriticalSection(系統范圍的臨界區段)

當有線程進入臨界區段時,其他線程必須等待。基於內核對象Mutex(互斥體)實現。

優點:可用於系統范圍內進程間同步,也可以用於進程內各線程間同步

缺點:效率低(有昂貴的用戶態切換到內核態的上下文切換)

平台 實現類
Windows typedef FWindowsSystemWideCriticalSection FSystemWideCriticalSection;
Mac typedef FMacSystemWideCriticalSection FSystemWideCriticalSection;
Unix typedef FUnixSystemWideCriticalSection FSystemWideCriticalSection;

Android

IOS

HoloLens

// 未實現

typedef FSystemWideCriticalSectionNotImplemented FSystemWideCriticalSection;

 

 

FRWLock(讀寫鎖)

由於讀線程並不會破壞數據,因此讀寫鎖將對鎖操作的線程分成:讀線程和寫線程。以提高並發效率。

讀線程以共享模式(share)來獲取和釋放鎖,寫線程以獨占模式(exclusive)來獲取和釋放鎖。

當沒有寫線程時,各個讀線程可並發運行。寫線程與寫線程是互斥的;寫線程與讀線程也是互斥的。

平台 實現類
Windows typedef FWindowsRWLock FRWLock;

Mac

Unix

Android

IOS

typedef FPThreadsRWLock FRWLock;
HoloLens typedef FHoloLensRWLock FRWLock;

 

FEvent(事件對象)

基於操作系統內核對象實現。ue4中通過FEventFPlatformProcess::CreateSynchEvent(bool bIsManualReset)來創建;或者調用FEvent* GetSynchEventFromPool(bool bIsManualReset)從緩存池里面獲取一個。

注:bIsManualReset為TRUE表示為手動重置事件;為FALSE表示為自動重置事件。操作系統將所有等待該Event線程切換到就緒后,會自動調用Reset將Event設置成未觸發狀態。因此,開發者自己不需要顯示地調用Reset。

執行Trigger函數,可將Event設置成觸發狀態;執行Reset函數,可將Event設置成未觸發狀態。

調用Wait函數來等待一個Event。注:Event處於未觸發狀態時,會阻塞等待。

平台 實現類
Windows

FEventWin : public FEvent

Mac

Unix

Android

IOS

FPThreadEvent : public FEvent

HoloLens FEventHoloLens : public FEvent

 

FSemaphore(信號量)

僅在windows平台上實現,詳見:FWindowsSemaphore

建議使用FEvent來替代

 

線程安全(Threadsafe)的容器

包括TArray,、TMapTSet在內的容器都不是線程安全的,需要自己對同步進行管理。

類型 解釋說明
TArrayWithThreadsafeAdd

TArray上派生

提供了AddThreadsafe函數來線程安全地往數組中添加元素

 

注1:不會引發擴容時,AddThreadsafe才線程安全

注2:其他的操作(AddRemoveEmpty等)都不是線程安全的

 

TLockFreePointerListFIFO

無鎖隊列(lock free queue),先進先出(FIFO)

 

基類FLockFreePointerFIFOBase

template<class T, int TPaddingForCacheContention, uint64 TABAInc = 1> // TABAInc為解決無鎖編程中ABA問題添加的參數值
class FLockFreePointerFIFOBase
{
  // 內存空隙(pad),防止cpu讀內存的高速緩存行(Cache Line)機制引發偽共享(False sharing)問題,導致急劇地性能下降
  FPaddingForCacheContention<TPaddingForCacheContention> PadToAvoidContention1;
  TDoublePtr Head; // 頭指針 被pad包裹
  FPaddingForCacheContention<TPaddingForCacheContention> PadToAvoidContention2;
  TDoublePtr Tail; // 尾指針 被pad包裹
  FPaddingForCacheContention<TPaddingForCacheContention> PadToAvoidContention3;
};

TLockFreePointerListUnordered

無鎖棧(lock free stack),后進先出(LIFO)

 

基類FLockFreePointerListLIFOBase

template<class T, int TPaddingForCacheContention, uint64 TABAInc = 1> // TABAInc為解決無鎖編程中ABA問題添加的參數值
class FLockFreePointerListLIFOBase
{
  FLockFreePointerListLIFORoot<TPaddingForCacheContention, TABAInc> RootList;
  {
    // 內存空隙(pad),防止cpu讀內存的高速緩存行(Cache Line)機制引發偽共享(False sharing)問題,導致急劇地性能下降
    FPaddingForCacheContention<TPaddingForCacheContention> PadToAvoidContention1;
    TDoublePtr Head; // 棧指針 被pad包裹
    FPaddingForCacheContention<TPaddingForCacheContention> PadToAvoidContention2;
  };
};

TLockFreePointerListLIFO

內存空隙為0的無鎖棧,后進先出(LIFO)

等價於TLockFreePointerListUnordered<T,TPaddingForCacheContention=0>

FStallingTaskQueue

無鎖任務隊列

 

線程在while循環里不斷的從隊列里取任務然后執行,如果隊列是空的,就會空跑while循環。雖然循環沒有多少邏輯,也是會耗費系統資源的

比較好的辦法就是讓線程在沒有任務執行時Wait等待,當把任務加進隊列之后再Trigger喚醒線程,繼續while循環

FStallingTaskQueue就是用來作此優化的類,stalling就是擱置線程的意思。另外它包含兩個FLockFreePointerFIFOBase無鎖隊列,作為高優先級和低優先級隊列

雖然真正讓線程Wait等待和Trigger喚醒的地方並不是在這個類里實現的,但它維護了各個線程的擱置狀態

這些狀態記錄在一個TDoublePtr MasterState里,還是這個64位指針,不過這次它的低26位不是指針了,而是表示26個線程的擱置狀態,1表示被擱置,可以被喚醒

高於26位的部分仍然作為標記計數,防止ABA問題

TQueue

基於鏈表(linked list)實現的不能插隊(non-intrusive)的無鎖隊列(lock free queue),先進先出(FIFO)

有兩種模式(EQueueMode):Mpsc(多生產者單消費者)和Spsc(單生產者單消費者)。

其中Spsc模式是無競爭(contention free)的。

從Head處入隊(Enqueue),從Tail處出隊(Dequeue)。示意圖如下:

   Tail                            Head
    |                               |
    V                               V
| Node C | --> | Node B | --> |  Node A | --> | nullptr |

 

TCircularQueue

基於數組(TArray)實現的循環無鎖隊列,先進先出(FIFO)

在僅有一個生產者和一個消費者時,線程安全

 

線程安全(Threadsafe)的Helper工具類

類型 解釋說明
FThreadSafeCounter

基於原子操作的FPlatformAtomics::Interlocked函數實現的線程安全的int32計數器

如:FThreadSafeCounter OutstandingHeartbeats;

FThreadSafeCounter64 基於原子操作的FPlatformAtomics::Interlocked函數實現的線程安全的int64計數器
FThreadSafeBool FThreadSafeCounter上派生實現的線程安全的Bool
TThreadSingleton 為每一個線程創建單例
FThreadIdleStats TThreadSingleton派生,用於計算各個線程的等待時間的統計邏輯
FMemStack TThreadSingleton派生,基於每個線程進行內存分配
TLockFreeFixedSizeAllocator 固定大小的lockfree的內存分配器
TLockFreeClassAllocator TLockFreeFixedSizeAllocator派生的對象內存分配器
FScopeLock

基於作用域的自旋鎖

利用c++的RAIIResource Acquisition is Initialization)特性(即:對象構造的時候其所需的資源便應該在構造函數中初始化,而對象析構的時候則釋放這些資源),基於FCriticalSection實現的自旋鎖(或旋轉鎖)。

{
    // CritSection為一個FCriticalSection臨界區段對象
    FScopeLock ScopeLock(&CritSection); // 如果CritSection被其他線程持有,則當前線程會在此等待

    // 臨界段(critical section)


    // 離開作用域,ScopeLock對象生命周期結束,會在其析構函數中釋放對CritSection臨界區段對象的占用
}

FScopedEvent

基於作用域的同步事件

利用c++的RAII(Resource Acquisition is Initialization)特性,基於FEvent實現的同步等待事件。

{
    FScopedEvent MyEvent;
    SendReferenceOrPointerToSomeOtherThread(&MyEvent); // 當其他線程中完成了任務時,調用MyEvent->Trigger(),將MyEvent變成觸發態

   // 離開作用域,會調用析構函數。MyEvent在析構函數中Wait任務的完成
}

 

參考

UE4異步操作總結

Concurrency & Parallelism in UE4 

【UE4源代碼觀察】學習隊列模板TQueue 

UE4的多線程——無鎖LockFree

UE並發-生產者消費者模式

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM