原作者:Madokakaroto
原文链接: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() {}
};
Init是对FRunnable对象的初始化,它是由FRunnableThread在创建线程对象后,进入线程函数的时候立即被FRunnableThread调用的函数,并不能由用户自己调用;Run是Runnable过程的入口,同样也是由FRunnableThread在Init成功后调用;Exit是Run正常退出后,由FRunnableThread调用,进行对FRunnable对象的清理工作;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 为 true 的情况无限循环。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 组合实现的,用于执行任务队列的线程池。