ue4-多线程使用

主线程异步或多线程异步的使用


在GameThread线程之外的其他线程中,不允许做一下事情

  • 不要 spawning / modifying / deleting UObjects / AActors

  • 不要使用定时器 TimerManager

  • 不要使用任何绘制接口,例如 DrawDebugLine

  • 如果想在主线程中异步处理(也就是分帧处理),可以使用一下接口(在 Async.h 中)

    1
    2
    3
    4
    AsyncTask(ENamedThreads::GameThread, [&]() {
    UE_LOG(LogMyTest, Warning, TEXT("--- UMyGameInstance::MyAsyncTask"));
    SpawnActor(3);
    });

多线程使用

新开线程线程的类

  • PrimeNumberWorker.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    class UMyGameInstance;

    //~~~~~ Multi Threading ~~~
    class FPrimeNumberWorker : public FRunnable
    {
    static FPrimeNumberWorker* Runnable;
    FRunnableThread* Thread;
    TArray<uint32>* PrimeNumbers;
    UMyGameInstance* mGameIns;
    FThreadSafeCounter StopTaskCounter; //用于多线程间的判断交互,volatile int32 Counter;
    int32 FindNextPrimeNumber();

    FCriticalSection QueueCritical; //互斥锁
    FEvent* ThreadSuspendedEvent; //线程悬挂和唤醒事件

    public:
    bool IsFinished();
    void Suspend();
    void Resume();

    //Constructor / Destructor
    FPrimeNumberWorker(TArray<uint32>& TheArray, UMyGameInstance* GameIns);
    virtual ~FPrimeNumberWorker();

    // Begin FRunnable interface.
    virtual bool Init();
    virtual uint32 Run();
    virtual void Stop();
    // End FRunnable interface

    /** Makes sure this thread has stopped properly */
    void EnsureCompletion();
    FCriticalSection* GetCriticalSection();

    static FPrimeNumberWorker* JoyInit(TArray<uint32>& TheArray, UMyGameInstance* GameIns);
    static FPrimeNumberWorker* Get();
    static void Shutdown();
    static bool IsThreadFinished();
    };
  • PrimeNumberWorker.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    #include "MyTest.h"
    #include "PrimeNumberWorker.h"
    #include "../MyGameInstance.h"

    FPrimeNumberWorker* FPrimeNumberWorker::Runnable = nullptr;
    FPrimeNumberWorker::FPrimeNumberWorker(TArray<uint32>& TheArray, UMyGameInstance* GameIns)
    : mGameIns(GameIns)
    , StopTaskCounter(0)
    {
    ThreadSuspendedEvent = FPlatformProcess::GetSynchEventFromPool();
    PrimeNumbers = &TheArray;
    Thread = FRunnableThread::Create(this, TEXT("FPrimeNumberWorker"), 0, TPri_BelowNormal); //windows default = 8mb for thread, could specify more
    }

    FPrimeNumberWorker::~FPrimeNumberWorker()
    {
    delete Thread;
    Thread = nullptr;
    FPlatformProcess::ReturnSynchEventToPool(ThreadSuspendedEvent);
    ThreadSuspendedEvent = nullptr;
    }

    bool FPrimeNumberWorker::Init()
    {
    PrimeNumbers->Empty();
    return true;
    }

    uint32 FPrimeNumberWorker::Run()
    {
    //Initial wait before starting
    FPlatformProcess::Sleep(0.03);
    while (StopTaskCounter.GetValue() == 0 && !IsFinished())
    {
    FScopeLock* QueueLock = new FScopeLock(&QueueCritical); //锁住
    //***************************************
    //不要 spawning / modifying / deleting UObjects / AActors 等等之类的事
    //这里做多线程间共享信息的 modify,如:PrimeNumbers->Add
    //***************************************
    PrimeNumbers->Add(FindNextPrimeNumber());
    UE_LOG(LogMyTest, Warning, TEXT("--- FPrimeNumberWorker::Run, lock"));
    //Suspend();
    //prevent thread from using too many resources
    FPlatformProcess::Sleep(1.0f); //这里睡眠3秒是为了让GameThread中的UMyGameInstance::MyAsyncThread中的日志打不出来
    delete QueueLock;//解锁
    UE_LOG(LogMyTest, Warning, TEXT("--- FPrimeNumberWorker::Run, unlock"));
    //FPlatformProcess::Sleep(2.0f); //这里睡眠2秒是为了让GameThread中获取到 互斥锁QueueCritical 并锁住
    }
    Stop();
    return 0;
    }

    bool FPrimeNumberWorker::IsFinished()
    {
    return PrimeNumbers->Num() == 5;
    }

    void FPrimeNumberWorker::Suspend()
    {
    ThreadSuspendedEvent->Wait();
    }

    void FPrimeNumberWorker::Resume()
    {
    ThreadSuspendedEvent->Trigger();
    }

    void FPrimeNumberWorker::Stop()
    {
    StopTaskCounter.Increment();
    }

    FPrimeNumberWorker* FPrimeNumberWorker::JoyInit(TArray<uint32>& TheArray, UMyGameInstance* GameIns)
    {
    if (!Runnable && FPlatformProcess::SupportsMultithreading())
    {
    Runnable = new FPrimeNumberWorker(TheArray, GameIns);
    }
    bool isSupport = FPlatformProcess::SupportsMultithreading();
    FString msg = isSupport ? "SupportsMultithread" : "dont SupportsMultithreading";
    UE_LOG(LogMyTest, Warning, TEXT("--- FPrimeNumberWorker::JoyInit, msg:%s"), *msg);
    return Runnable;
    }

    FPrimeNumberWorker* FPrimeNumberWorker::Get()
    {
    return Runnable;
    }

    void FPrimeNumberWorker::EnsureCompletion()
    {
    Stop();
    Thread->WaitForCompletion();
    }

    FCriticalSection* FPrimeNumberWorker::GetCriticalSection()
    {
    return &QueueCritical;
    }

    void FPrimeNumberWorker::Shutdown()
    {
    if (Runnable)
    {
    Runnable->EnsureCompletion();
    delete Runnable;
    Runnable = nullptr;
    }
    }

    bool FPrimeNumberWorker::IsThreadFinished()
    {
    if (Runnable) return Runnable->IsFinished();
    return true;
    }

    int32 FPrimeNumberWorker::FindNextPrimeNumber()
    {
    int32 TestPrime = 123;
    return TestPrime;
    }
  • 再来个测试方法,写在GameInstanece的扩展类中(习惯了测试用 console命令调方法,不懂的看这里 ue4-控制台执行方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    UFUNCTION(Exec)
    virtual void MyAsyncSuspend();
    UFUNCTION(Exec)
    virtual void MyAsyncResume();
    UFUNCTION(Exec)
    virtual void MyAsyncThread();

    void UMyGameInstance::MyAsyncSuspend()
    {
    FPrimeNumberWorker::Get()->Suspend();
    }

    void UMyGameInstance::MyAsyncResume()
    {
    FPrimeNumberWorker::Get()->Resume();
    }

    void UMyGameInstance::MyAsyncThread()
    {
    mNumVec.Empty();
    FPrimeNumberWorker::JoyInit(mNumVec, this);
    GetTimerManager().SetTimer(mTimer1, [&]()->void {
    FPrimeNumberWorker* pnw = FPrimeNumberWorker::Get();
    if (!pnw) return;

    FCriticalSection* cs = pnw->GetCriticalSection(); //获取FPrimeNumberWorker到中的互斥锁QueueCritical
    FScopeLock QueueLock(cs);//锁住,等作用域过后QueueLock自动析构解锁
    UE_LOG(LogMyTest, Warning, TEXT("--- UMyGameInstance::MyAsyncThread, mNumVec.Num=%d"), mNumVec.Num());
    if (pnw->IsThreadFinished())
    FPrimeNumberWorker::Shutdown();
    }, 1.0f, true);
    }
    1. 测试互斥。 上面的 FPrimeNumberWorker 里面的代码方法是有加锁的,在控制台输入指令 MyAsyncThread 可测试。注释掉 FScopeLock QueueLock(cs); 这一行代码就能看出没有互斥锁的区别。
    2. 测试线程的悬挂和唤醒。
      1. 取消注释 FPrimeNumberWorker 里面的 //Suspend();
      2. 注释掉 MyAsyncThread 测试方法中的的定时器 GetTimerManager(因为其他线程中在 未解锁 的 情况下 悬挂 的话,主线程获取不到这个锁会一直等待,也就是看起来像卡死了)
      3. 在控制台输入指令 MyAsyncThread,就可以 Run 方法的线程悬挂住该线程
      4. 在控制台输入指令 MyAsyncResume,就可以 唤醒之前调用 ThreadSuspendedEvent->Wait(); 的线程。

线程唤醒、悬挂

  • FEvent* ThreadSuspendedEvent;
  • 线程悬挂。A线程中调用 ThreadSuspendedEvent->Wait();,A线程就会悬挂主,必须由B线程调用 ThreadSuspendedEvent->Trigger(); 才能唤醒A线程。
  • 所以如果直接在主线程(GameThread)中调用 ThreadSuspendedEvent->Wait();,那就看起像卡死了。

编辑器模式下进行多线程开发注意事项

  • PIE模式下,与新开的线程交互正常;如果退出PIE模式,PIE中的实例对象都会被标记为坐等回收的对象 InValid,此时如果新开的线程还在Run中跑用到PIE中的实例对象,将造成 编辑器崩溃

  • 最好在你的游戏退出时 FPrimeNumberWorker ShutDown一下,方便下次测试。

    1
    2
    3
    4
    5
    void UMyGameInstance::Shutdown()
    {
    FPrimeNumberWorker::Shutdown();
    Super::Shutdown();
    }

参考资料