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; |
示例代码:
FCriticalSection EventsMutex; EventsMutex.Lock(); // ... EventsMutex.Unlock();
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中通过FEvent* FPlatformProcess::CreateSynchEvent(bool bIsManualReset)来创建;或者调用FEvent* FPlatformProcess::GetSynchEventFromPool(bool bIsManualReset)从缓存池里面获取一个。
不再使用时,调用FPlatformProcess::ReturnSynchEventToPool(FEvent* Event)来将FEvent对象回收给缓存池。
注1:bIsManualReset为TRUE表示为手动重置事件;为FALSE表示为自动重置事件。操作系统将所有等待该Event线程切换到就绪后,会自动调用Reset将Event设置成未触发状态。因此,开发者自己不需要显示地调用Reset。
执行Trigger函数,可将Event设置成触发状态;执行Reset函数,可将Event设置成未触发状态。
调用Wait函数来等待一个Event。注:Event处于未触发状态时,会阻塞等待。
注2:为了方便使用,UE还提供了从FEvent上派生的FSafeRecyclableEvent,以及FEventPool。
平台 | 实现类 |
Windows |
FEventWin : public FEvent |
Mac Unix Android IOS |
FPThreadEvent : public FEvent |
HoloLens | FEventHoloLens : public FEvent |
FSemaphore(信号量)
仅在windows平台上实现,详见:FWindowsSemaphore
建议使用FEvent来替代
线程安全(Threadsafe)的容器
包括TArray、TMap、TSet在内的容器都不是线程安全的,需要自己对同步进行管理。
类型 | 解释说明 |
TArrayWithThreadsafeAdd |
从TArray上派生 提供了AddThreadsafe函数来线程安全地往数组中添加元素
注1:不会引发扩容时,AddThreadsafe才线程安全 注2:其他的操作(Add、Remove、Empty等)都不是线程安全的
|
TLockFreePointerListFIFO |
无锁队列(lock free queue),带内存空隙(pad),先进先出(FIFO)
基类FLockFreePointerFIFOBase template<class T, int TPaddingForCacheContention, uint64 TABAInc = 1> // TABAInc为解决无锁编程中ABA问题添加的参数值 |
TLockFreePointerListUnordered |
无锁栈(lock free stack),带内存空隙(pad),后进先出(LIFO)
基类FLockFreePointerListLIFOBase template<class T, int TPaddingForCacheContention, uint64 TABAInc = 1> // TABAInc为解决无锁编程中ABA问题添加的参数值 |
TLockFreePointerListLIFOPad |
无锁栈(lock free stack),带内存空隙(pad),后进先出(LIFO)
基类FLockFreePointerListLIFOBase |
TClosableLockFreePointerListUnorderedSingleConsumer |
带close状态的无锁栈(lock free stack),后进先出(LIFO)
基类FLockFreePointerListLIFOBase 在TaskGraph的FGraphEvent类中使用该栈记录依赖该Event的所有FBaseGraphTask对象 |
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; OutstandingHeartbeats.Set(0); // 将OutstandingHeartbeats设为0 OutstandingHeartbeats.Set(1); // 将OutstandingHeartbeats设为1 OutstandingHeartbeats.Reset(); // 等价于OutstandingHeartbeats.Set(0) |
FThreadSafeCounter64 | 基于原子操作的FPlatformAtomics::Interlocked函数实现的线程安全的int64计数器 |
FThreadSafeBool | 从FThreadSafeCounter上派生实现的线程安全的Bool |
TThreadSingleton | 为每一个线程创建单例 注:普通的单例类为TLazySingleton<Type>,如:TLazySingleton<FCoreTicker>::Get()来获得FCoreTicker的单例 |
FThreadIdleStats | 从TThreadSingleton派生,用于计算各个线程的等待时间的统计逻辑 |
FMemStack | 从TThreadSingleton派生,基于每个线程进行内存分配 |
TLockFreeFixedSizeAllocator | 固定大小的lockfree的内存分配器 |
TLockFreeClassAllocator | 从TLockFreeFixedSizeAllocator派生的对象内存分配器 |
FScopeLock |
基于作用域的自旋锁 利用c++的RAII(Resource Acquisition is Initialization)特性(即:对象构造的时候其所需的资源便应该在构造函数中初始化,而对象析构的时候则释放这些资源),基于FCriticalSection实现的自旋锁(或旋转锁)。 { |
FRWScopeLock |
基于作用域的读写锁 利用c++的RAII特性 static FRWLock KnownMissingLock; // 读写锁 bool FCoreRedirects::IsKnownMissing(ECoreRedirectFlags Type, const FCoreRedirectObjectName& ObjectName) FRWScopeLock ScopeLock(KnownMissingLock, FRWScopeLockType::SLT_ReadOnly); bool FCoreRedirects::AddKnownMissing(ECoreRedirectFlags Type, const FCoreRedirectObjectName& ObjectName, ECoreRedirectFlags Channel) FRWScopeLock ScopeLock(KnownMissingLock, FRWScopeLockType::SLT_Write); bool FCoreRedirects::RemoveKnownMissing(ECoreRedirectFlags Type, const FCoreRedirectObjectName& ObjectName, ECoreRedirectFlags Channel) FRWScopeLock ScopeLock(KnownMissingLock, FRWScopeLockType::SLT_Write); |
FScopedEvent |
基于作用域的同步事件 利用c++的RAII(Resource Acquisition is Initialization)特性,基于FEvent实现的同步等待事件。 { |
综合示例:UE4中的FMediaTicker实现
/****************************** UnrealEngine\Engine\Source\Runtime\Media\Private\MediaTicker.h ********************************/ #pragma once #include "Containers/Array.h" #include "HAL/CriticalSection.h" #include "HAL/Runnable.h" #include "Templates/Atomic.h" #include "Templates/SharedPointer.h" #include "IMediaTicker.h" class FEvent; class IMediaTickable; /** * High frequency ticker thread. */ class FMediaTicker : public FRunnable , public IMediaTicker { public: /** Default constructor. */ FMediaTicker(); /** Virtual destructor. */ virtual ~FMediaTicker(); public: //~ FRunnable interface virtual bool Init() override; virtual uint32 Run() override; virtual void Stop() override; virtual void Exit() override; public: //~ IMediaTicker interface virtual void AddTickable(const TSharedRef<IMediaTickable, ESPMode::ThreadSafe>& Tickable) override; virtual void RemoveTickable(const TSharedRef<IMediaTickable, ESPMode::ThreadSafe>& Tickable) override; protected: /** Tick all tickables. */ void TickTickables(); private: /** Critical section for synchronizing access to high-frequency clock sinks. */ FCriticalSection CriticalSection; /** Holds a flag indicating that the thread is stopping. */ TAtomic<bool> Stopping; /** Collection of tickable objects. */ TArray<TWeakPtr<IMediaTickable, ESPMode::ThreadSafe>> Tickables; /** Variable to avoid rellocating the Tickables array repeatedly. */ TArray<TWeakPtr<IMediaTickable, ESPMode::ThreadSafe>> TickablesCopy; /** Holds an event signaling the thread to wake up. */ FEvent* WakeupEvent; }; /********************************************************************************************************************************/ /****************************** UnrealEngine\Engine\Source\Runtime\Media\Private\MediaTicker.cpp ********************************/ /********************************************************************************************************************************/ #include "HAL/Event.h" #include "HAL/PlatformProcess.h" #include "Misc/ScopeLock.h" #include "IMediaTickable.h" /* FMediaTicker structors *****************************************************************************/ FMediaTicker::FMediaTicker() : Stopping(false) { WakeupEvent = FPlatformProcess::GetSynchEventFromPool(true); } FMediaTicker::~FMediaTicker() { FPlatformProcess::ReturnSynchEventToPool(WakeupEvent); WakeupEvent = nullptr; } /* FRunnable interface *****************************************************************************/ bool FMediaTicker::Init() { return true; } uint32 FMediaTicker::Run() { while (!Stopping) { if (WakeupEvent->Wait() && !Stopping) { TickTickables(); if (!Stopping) { FPlatformProcess::Sleep(0.005f); } } } return 0; } void FMediaTicker::Stop() { Stopping = true; WakeupEvent->Trigger(); } void FMediaTicker::Exit() { // do nothing } /* IMediaTicker interface *****************************************************************************/ void FMediaTicker::AddTickable(const TSharedRef<IMediaTickable, ESPMode::ThreadSafe>& Tickable) { FScopeLock Lock(&CriticalSection); Tickables.AddUnique(Tickable); WakeupEvent->Trigger(); } void FMediaTicker::RemoveTickable(const TSharedRef<IMediaTickable, ESPMode::ThreadSafe>& Tickable) { FScopeLock Lock(&CriticalSection); Tickables.Remove(Tickable); } /* FMediaTicker implementation *****************************************************************************/ void FMediaTicker::TickTickables() { TickablesCopy.Reset(); { FScopeLock Lock(&CriticalSection); for (int32 TickableIndex = Tickables.Num() - 1; TickableIndex >= 0; --TickableIndex) { TSharedPtr<IMediaTickable, ESPMode::ThreadSafe> Tickable = Tickables[TickableIndex].Pin(); if (Tickable.IsValid()) { TickablesCopy.Add(Tickable); } else { Tickables.RemoveAtSwap(TickableIndex); } } if (Tickables.Num() == 0) { WakeupEvent->Reset(); } } for (int32 i=0; i < TickablesCopy.Num(); ++i) { TSharedPtr<IMediaTickable, ESPMode::ThreadSafe> Tickable = TickablesCopy[i].Pin(); if (Tickable.IsValid()) { Tickable->TickTickable(); } } }