UE4异步编程专题 - 多线程

专题的第二篇,我们聊聊UE4中的多线程的基础设施。UE4中最基础的模型就是FRunnable和FRunnableThread,FRunnable抽象出一个可以执行在线程上的对象,而FRunnableThread是平台无关的线程对象的抽象。后面的篇幅会详细讨论这些基础设施。

1. FRunnable

UE4为我们抽象FRunnable的概念,让我们指定在线程上运行的一段逻辑过程。该过程通常是一个较为耗时的操作,例如文件IO;或者是常态为空闲等待(Idle)的循环,随时等待新执行命令到来。

FRunnable为我们提供了四个重要的接口:

class CORE_API FRunnable
{
public:
    // ....
    virtual bool Init();
    virtual uint32 Run() = 0;
    virtual void Stop() {}
    virtual void Exit() {}
};
  1. Init是对FRunnable对象的初始化,它是由FRunnableThread在创建线程对象后,进入线程函数的时候立即被FRunnableThread调用的函数,并不能由用户自己调用;
  2. Run是Runnable过程的入口,同样也是有FRunnableThread在Init成功后调用;
  3. Exit是Run正常退出后,由FRunnableThread调用,进行对FRunnable对象的清理工作;
  4. Stop是给用户使用的接口,当我们觉得必要时停止FRunnable.

例如一个空闲等待的FRunnable的实现:

class MyRunnable : public FRunnable
{
public:
    MyRunnable()
        : RunningFlag(false)
        , WorkEvent(FPlatformProcess::GetSynchEventFromPool())
    {}

    ~MyRunnable()
    {
        FPlatformProcess::ReturnSynchEventToPool(WorkEvent);
        WorkEvent = nullptr;
    }

    bool Init() override
    {
        // ..
        if(!WorkEvent)
            return false;
        RunningFlag.Store(true);
    }

    void Run() override
    {
        while(RunningFlag.Load())
        {
            WorkEvent->Wait(MAX_uint32);
            if(!RunningFlag.Load())
                break;
            // ...
        }
    }

    void Stop() override
    {
        if(RunningFlag.Exchange(false))
            WorkEvent->Trigger();
    }

    void Exit() overrdie
    {
        // ...
        RunningFlag.Store(false);
    }

    void Notify()
    {
        WorkEvent->Trigger();
    }

private:
    TAtomic<bool>   RunningFlag;
    FEvent*         WorkEvent;
    // ...
};

原子变量RunningFlag作为Runnable对象的运行状态的标记,所以Run函数的主体就是在RunningFlag为ture的情况无限循环。WorkEvent是其他线程上执行的任务与MyRunnable交互的事件对象,通过Notify接口,可以唤醒它继续执行。MyRunnable从Wait中醒来时,还会检查一次RunningFlag,有可能是唤醒它的是Stop接口发出的事件。而Stop的实现,会判断一下标识是否Runnable已经退出,而不用再次发出事件了。

2. FRunnableThread

FRunnable需要依附与一个FRunnableThread对象,才能被执行。例如,我们如果要执行第一节的空闲等待的Runnable:

auto* my_runnable = new MyRunnable{};

auto* runnable_thread = FRunnableThread::Create(my_runnable, "IdleWait");

FRunnableThread是平台无关的线程对象的抽象,它驱动着FRunnable的初始化,执行和清理,并提供了管理线程对象生命周期,线程局部存储,亲缘性和优先级等接口。

class FRunnableThread
{
    // ....
    // Tls 索引
    static uint32 RunnableTlsSlot;

public:
    // 获取Tls索引
    static uint32 GetTlsSlot();

    // 平台无关的创建线程对象接口
    static FRunnableThread* Create(
        class FRunnable* InRunnable,
        const TCHAR* ThreadName,
        uint32 InStackSize,
        EThreadPriority InThreadPri,
        uint64 InThreadAffinityMask);

public:
    // 设置线程优先级
    virtual void SetThreadPriority( EThreadPriority NewPriority ) = 0;

    // 挂起线程
    virtual void Suspend( bool bShouldPause = true ) = 0;

    // 杀死线程
    virtual bool Kill( bool bShouldWait = true ) = 0;

    // 同步等待线程退出
    virtual void WaitForCompletion() = 0;

protected:
    // 平台相关的线程对象初始化过程
    virtual bool CreateInternal(
        FRunnable* InRunnable, 
        const TCHAR* InThreadName,
        uint32 InStackSize,
        EThreadPriority InThreadPri, 
        uint64 InThreadAffinityMask) = 0;
};

UE4已经实现了各个平台的线程对象。Win平台使用的是系统Windows的Thread API. 而其他平台是基于pthread,不同平台实现上略有不同。通过编译选项包含平台相关的头文件,并通过FPlatformProcess类型的定义来选择相应平台的实现。参见FRunnableThread::Create函数:

FRunnableThread* FRunnableThread::Create(
    class FRunnable* InRunnable, 
    const TCHAR* ThreadName,
    uint32 InStackSize,
    EThreadPriority InThreadPri, 
    uint64 InThreadAffinityMask)
{
    // ....
    NewThread = FPlatformProcess::CreateRunnableThread();
    if (NewThread)
    {
        if (NewThread->CreateInternal(...))
        // .....
    }
    // ....
}

线程对象的创建,需要指定一个FRunnable对象的实例。

FPlatformProcess::CreateRunnableThread就是简单地new一个平台相关的线程对象,而真正的初始化时在FRunnableThread::CreateInternal当中完成的。线程平台相关的API差异很大,UE4的封装尽可能地让各个平台的实现略有不同。

系统API创建的线程对象,都以_ThreadProc作为入口函数。接下来是一系列的平台相关的初始化工作,例如设置栈的大小,TLS的索引,亲缘性掩码,获取平台相关的线程ID等。之后,就会进入上一节我们提及的FRunnable的初始化流程中了。一个线程创建成功的时序图如下:

Win平台的实现中,由于API的历史原因需要_ThreadProc的调用约定是STDCALL. 因此Win平台下的_ThreadProc函数,是一个转发函数,转发给了另外一个CDECL调用约定的函数FRunnableThreadWin::GuardedRun.

3. Runnable or Callable

UE4的多线程模型是Runnable和Thread,但是有不少C++库,如标准库,是Callable and Thread. 如果使用标准库的std::thread:

int main(void)
{
    std::thread t{ [](){ std::cout << "Hello Thread." } };

    t.join();
    return 0;
}

暂时忽略标准库thread简陋的设施,Callable和Runnable这两个模型是可以等价的,也就是他们可以相互表达。

例如我们可以用UE4的设施,实现类似std::thread的FThread(UE4已经实现了一个):

class FThread final : public FRunnable
{
public:
    template <typename Func, typename ... Args>
    explicit FThread(Func&& f, Args&& ... args)
        : Callable(create_callable(
            std::forward<Func>(f), 
            std::forward<Args>(args)...
         ))
        , Thread(FRunnableThread::Create(this, "whatever"))
    {
        if(!Thread)
            throw std::runtime_error{ "Failed to create thread!" };
    }

    void join()
    {
        Thread->WaitForCompletion();
    }

    virtual uint32 Run() override
    {
        Callable();
        return 0;
    }

private:
    template <typename Func, typename ... Args>
    static auto create_callable(Func&& f, Args&& ... args) noexcept
    {
        // 为了简单起见用了20的特性,如果是17标准以下的话,用tuple也能办到。
        // Eat return type
        return [func = std::forward<Func>(f), ... args = std::forward<Args>(args)]()
        {
            std::invoke(func, std::forward<Args>(args...));
        };
    }

private:
    TFunction<void()>   Callable;
    FRunnableThread*    Thread;
};

我们还可以用std::thread和一些封装,来实现一个的RunnableThread. 下面是一个简单的实现:

class RunnableThread
{
public:
    explicit RunnableThread(FRunnable* runnable)
        : runnable_(runnable)
        , inited_(false)
        , init_result_(false)
        , thread_(&RunnableThread::Run, this)
    {
        std::unique_lock<std::mutex> lock{ mutex_ };
        cv_.wait(lock, [this](){ return inited_; });
    }

protected:
    void Run()
    {
        auto result = runnable_->Init();
        {
            std::unique_lock<std::mutex> lock{ mutex_ };
            inited_ = true;
            init_result_ = result;
        }
        cv_.notify_one();

        if(result)
        {
            runnable_->Run();
            runnable_->Exit();
        }
    }

private:
    FRunnable*              runnable_;
    bool                    inited_;
    bool                    init_result_;
    std::thread             thread_;
    std::mutex              mutex_;
    std::condition_variable cv_;
};

虽然笔者不喜欢面向对象的设计(OOD),但UE4的FRunnable和FRunnaableThread实现得确实挺不错。没有很重的框架束缚,并且FRunnable也有着跟callable一样的表达能力,并且FRunnableThread封装了各个平台线程库几乎所有的功能特性。总体上来说,比标准库的thread设施更齐全。

4. 小结

UE4中的多线程模型用一句话概括为: A FRunnable runs on a FRunnableThread.

FRunnable是逻辑上的可执行对象的概念的抽象。对于一个具体的可执行对象的实现,用户需要实现Init和Exit接口,对Runnable需要的系统资源进行申请和释放;用户需要实现Run来控制Runnable的执行流程,并在需要的情况下实现Stop接口,来控制Runnable的退出。

FRunnableThread是UE4提供的平台无关的线程对象的抽象,并提供了控制线程对象生命周期和状态的接口。UE4实现了常见所有平台的线程对象,实现细节对用户透明。

除此之外,本文还讨论了Runnable与Callable两种模型,并且它们具有相同的表达能力。

这个系列的下一篇,将会讨论FQueuedThreadPool. 它是由FRunnable及FRunnableThread组合实现的,用于执行任务队列的线程池。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在UE4中,可以通过使用多线程来运行插件,以提高程序的性能和并行处理能力。 首先,插件在UE4中是以模块的形式存在的,可以独立于游戏项目而存在。为了实现多线程运行插件,可以在插件的模块中编写异步任务,利用多线程来执行这些任务。 在UE4中,我们可以使用FRunnable接口来创建自定义线程。首先,需要创建一个继承自FRunnable的类,并实现必要的接口函数,如Run和Stop。在Run函数中,编写插件需要执行的任务代码。 在插件的初始化阶段,可以通过调用FRunnableThread::Create函数来创建一个新的线程,并指定我们刚刚定义的FRunnable类对象作为参数。然后,可以开始执行插件任务。 需要注意的是,在多线程编程中,需要合理地处理线程之间的数据共享和同步问题。可以使用一些线程同步的机制,如互斥锁、信号量等,来避免多个线程同时操作共享数据导致的冲突。 另外,UE4还提供了一些已经封装好的多线程工具类,如FGraphEvent和FQueuedThread等,可以帮助我们更方便地实现多线程任务的管理和同步。 总结来说,UE4中可以通过使用FRunnable接口和多线程工具类来实现插件的多线程运行。合理地设计和管理多线程任务,可以提高程序的性能和并行处理能力,并且确保线程之间的数据共享和同步的正确性。 ### 回答2: 在UE4中,我们可以通过多线程来实现插件的运行。多线程是一种并发执行任务的方式,可以让程序同时执行多个任务,提高了程序的效率和性能。 在UE4中,我们可以使用一些多线程的技术来实现插件的运行。比如,使用C++中的std::thread来创建多个线程,并让每个线程在后台执行插件相关的任务。同时,我们还可以使用一些线程同步的机制,比如互斥锁、条件变量等来管理多个线程之间的资源访问和共享。 在插件的运行过程中,可以将一些耗时的任务放在单独的线程中执行,这样就不会阻塞主线程的执行。比如,可以将插件中的某个算法或者处理逻辑放在一个独立的线程中运行,这样主线程可以继续执行其他的任务,提高了程序的响应速度和用户体验。 除了使用多线程,还可以使用UE4中提供的任务图谱系统来实现插件的并行执行。任务图谱系统可以将任务划分成多个小任务,并按照依赖关系进行调度,从而充分利用计算资源并提高任务执行的效率。 总结来说,UE4中可以通过多线程技术来实现插件的运行,可以提高程序的效率和性能,同时还可以使用任务图谱系统来实现任务的并行执行。这些技术可以充分发挥计算资源的利用率,提高插件的运行效果和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值