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


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

  • 不要 spawning / modifying / deleting UObjects / AActors
  • 不要使用定时器 TimerManager
  • 不要使用任何绘制接口,例如 DrawDebugLine

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

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

多线程使用

新开线程线程的类

  • PrimeNumberWorker.h

    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

    
    #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-控制台执行方法

    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一下,方便下次测试。

    void UMyGameInstance::Shutdown()
    {
        FPrimeNumberWorker::Shutdown();
        Super::Shutdown();
    }

参考资料

Logo

Agent 垂直技术社区,欢迎活跃、内容共建,欢迎商务合作。wx: diudiu5555

更多推荐