ArkUI中的线程模型

HarmonyOS 在演进过程中,先后提出了两种应用模型:FA(Feature Ability)模型Stage模型 。其中,FA模型是早期推出的,支持两种开发范式:类Web范式声明式范式 ;而Stage模型作为当前主推的应用模型,仅支持声明式范式。类Web范式与声明式范式最直观的区别在于:前者基于JavaScript语言,所构建的应用中每个页面对应三个文件(.js、.hml和.css);后者基于ArkTS语言(扩展的TypeScript语言),所构建的应用中每个页面对应一个文件(.ets)。

本文不过多关注HarmonyOS中应用模型和开发范式的具体内容,而是聚焦于支撑应用构建用户界面的ArkUI开发框架中的线程模型。后文将对ArkUI线程模型的代码实现以及在不同应用模型和开发范式上的区别进行详细介绍。

1. 线程模型组成

本小节将对ArkUI线程模型的组成结构进行介绍,同时指出不同开发范式以及应用模型在线程模型上的不同之处。

ArkUI框架将任务类型分为如下六种:Platform、JS、UI、GPU、IO和Background。其中,Background类型任务由一个线程池(默认包含八个线程)负责处理,其余类型任务均由单个线程承载。具体的,各类任务对应的线程功能如下:

类型 功能
Platform线程 当前应用的主线程,主要负责平台层的交互、应用生命周期以及窗口环境的创建
JS线程 JS前端框架的执行线程,应用的JS逻辑以及应用UI界面的解析构建都在该线程执行
UI线程 引擎的核心线程,组件树的构建以及整个渲染管线的核心逻辑都在该线程:包括渲染树的构建、布局、绘制以及动画调度
GPU线程 现代渲染引擎,为充分发挥硬件性能,都支持GPU硬件加速。在该线程上,会通过系统的窗口句柄,创建GPU加速的OpenGL环境,负责将整个渲染树的内容光栅化,直接将每一帧的内容渲染合成到该窗口的Surface上并送显
IO线程 主要为了异步的文件IO读写,同时该线程会创建一个离屏的GL环境,这个环境和 GPU线程的GL环境是同一个共享组,可以共享资源,图片资源解码的内容可直接在该线程上传生成GPU纹理,实现更高效的图片渲染
Background线程池 用于执行低优先级的可并行异步任务,如网络请求、Asset资源加载等

需要说明的是,ArkUI中不同应用模型或不同开发范式对应的线程模型是有差异的。区别如下:

  • 类Web范式:Platform、JS和UI类型的任务会由三个不同的线程执行。
  • 声明式范式FA模型:Platform类型任务由一个线程执行,JS和UI类型由同一个线程执行。
  • 声明式范式Stage模型:Platform、JS和UI类型的任务共用同一个线程执行。

另外,目前ArkUI中,IO类型任务会与UI类型任务共用同一个线程执行。且在使能Rosen后端后,GPU类型任务会与UI类型任务也会共用同一个线程执行。后续介绍代码时,会再做说明。

2. 线程模型实现

本小节将对ArkUI线程模型相关代码逐个说明。ArkUI代码仓中,与线程模型相关的主要是下图中的几个文件:

arduino 复制代码
.
├── adapter
│   ├── ohos
│   │   └── entrance
│   │       ├── ace_container.h
│   │       └── ace_container.cpp
│   └── preview
│       └── entrance
│           ├── ace_container.h
│           └── ace_container.cpp
└── frameworks
    ├── base
    │   └── thread
    │       ├── background_task_executor.h
    |       ├── background_task_executor.cpp
    │       ├── cancelable_callback.h
    |       ├── task_executor.h
    |       └── frame_trace_adapter.h
    └── core
        └── common
            └── flutter
                ├── flutter_task_executor.h
                ├── flutter_task_executor.cpp
                ├── flutter_thread_model.h
                └── flutter_thread_model.cpp

在ArkUI框架中,各种类型的任务是通过任务执行器进行管理的,是整个线程模型的核心。

  1. 任务执行器 定义于文件tast_executor.h和flutter_task_executor.h (.cpp) 中,前者定义了任务执行器的基类TaskExecutor ,后者定义了任务执行器基于Flutter线程库的实现类FlutterTaskExecutor
  2. FlutterTaskExecutor 中,Background类型的任务会交给后台任务执行器 去处理,由文件background_task_executor.h (.cpp) 中的BackgroundTaskExecutor 类定义。其他类型的任务由基于flutter线程库创建的五个fml::TaskRunner实例进行处理。
  3. 文件cancelable_callback.h中,给出了一个基本任务的描述,定义为类CancelableCallback ,由其内部类Callback辅助实现。

除此之外,ace_container.h (.cpp) 和flutter_thread_model.h (.cpp) 主要和任务执行器的创建相关。

2.1 任务执行器

任务执行器是整个线程模型的核心,负责将各种类型的任务交给对应的线程或者线程池进行执行。本小节分将对任务执行器的基类TaskExecutor 和基于Flutter中线程库的实现类FlutterTaskExecutor进行介绍。

基类TaskExecutor

在TaskExecutor中,主要给出了ArkUI中任务类型的定义 以及用于抛同步和异步任务的接口

  1. 如前文所述,ArkUI中的任务类型分为六类,与之呼应,可以看到在tast_executor.h中定义了如下枚举类TaskType
kotlin 复制代码
enum class TaskType : uint32_t {
    PLATFORM = 0,
    UI,
    IO,
    GPU,
    JS,
    BACKGROUND,
    UNKNOWN,
};

在整个ArkUI仓中,将某个任务抛到对应线程执行时,都会以此枚举类型进行标定。

  1. 下表对TaskExecutor中提供的所有接口进行简单分类,以说明其作用:

接口 功能 分类
TaskExecutor() 构造和析构函数 NA
~TaskExecutor()
virtual void Destory() 用于释放任务执行器所占用的线程资源
virtual int32_t GetTid(TaskType type) 用于获取执行指定类型任务的线程ID
virtual uint32_t GetTotalTaskNum(TaskType type) 用于获取执行指定类型任务的线程任务队列中的任务数量
virtual bool WillRunOnCurrentThread(TaskType type) const = 0 用于判断执行抛任务的线程与任务类型是否一致(上述以Post开始的接口)
bool PostTaskAndWait(CancelableTask&& task, TaskType type, std::chrono::milliseconds timeoutMs = 0ms) const 用于抛同步任务的底层接口,所有抛同步任务的接口均间接调用此接口 同步任务相关接口(若判断执行抛任务动作的线程与任务的执行线程一致,则直接执行任务)
bool PostSyncTask(Task&& task, TaskType type) const 将同步任务抛到指定线程
bool PostSyncTask(const Task& task, TaskType type) const
bool PostSyncTask(CancelableTask&& task, TaskType type) const
bool PostSyncTask(const CancelableTask& task, TaskType type) const
bool PostSyncTaskTimeout(const Task& task, TaskType type, uint32_t timeoutMs) const 将同步任务抛到指定线程并设置超时时间,若超过设定时间任务还未执行,则取消任务
virtual bool OnPostTask(Task&& task, TaskType type, uint32_t delayTime) const = 0 用于抛异步任务的底层接口,所有抛异步任务的接口均间接调用此接口 异步任务相关接口
bool PostDelayedTask(Task&& task, TaskType type, uint32_t delayTime) const 将异步延时任务抛到指定线程
bool PostDelayedTask(const Task& task, TaskType type, uint32_t delayTime) const
bool PostTask(Task&& task, TaskType type) const 将延时为0的异步任务抛到指定线程
bool PostTask(const Task& task, TaskType type) const
virtual Task WrapTaskWithTraceId(Task&& task, int32_t id) const = 0 将TraceId与所要执行的任务打包到一起
bool PostTaskWithTraceId(Task&& task, TaskType type, int32_t id) const 将带有TraceId的延时为0的异步任务抛到指定线程,TraceId用于日志中
bool PostTaskWithTraceId(const Task& task, TaskType type, int32_t id) const
virtual void AddTaskObserver(Task&& callback) = 0 添加或移除可多次执行的任务 可多次执行的任务相关接口
virtual void RemoveTaskObserver() = 0

可以看到,TaskExecutor类中主要是给出了抛同步和异步任务的接口,基本每个接口的形参都有左、右值引用两种形式。在平时开发中,为了提高一丝性能,可以优先选择调用形参为右值的接口,这类接口可以省去一些创建和拷贝实参的性能消耗。具体到代码,可以看到TaskExecutor类提供的接口实现并不复杂,需要特别说明的是:

  • 异步类型的接口:最终都会调用到虚接口OnPostTask;该接口的实现将在介绍子类FlutterTaskExecutor时进行详细说明;

  • 同步类型的接口:

    • 判断是否为Background类型的任务,若是,则直接返回false,即不允许抛Background类型的同步任务。
    • 调用WillRunOnCurrentThread判断当前任务是否刚好在当前线程执行,如果是,则直接执行该任务。否则调用接口PostTaskAndWait
    • PostTaskAndWait接口调用了虚接口OnPostTask和CancelableCallback中的WaitUntilComplete方法;后者的实现将在介绍类CancelableCallback时进行详细说明;

上述两点是实现同、异步执行任务的关键接口。

基于Flutter线程库的实现类FlutterTaskExecutor

FlutterTaskExecutor 是基于Flutter线程库进行实现的。前文提到的Platform、JS、UI、GPU和IO这五种类型的任务是交由基于flutter线程库创建的五个fml::TaskRunner 实例进行处理的。对应FlutterTaskExecutor中的如下属性:

arduino 复制代码
fml::RefPtr<fml::TaskRunner> platformRunner_;
fml::RefPtr<fml::TaskRunner> uiRunner_;
fml::RefPtr<fml::TaskRunner> ioRunner_;
fml::RefPtr<fml::TaskRunner> jsRunner_;
fml::RefPtr<fml::TaskRunner> gpuRunner_;

OnPostTask方法中,可以看到这五种类型的fml::TaskRunner实例会负责处理对应类型的任务。

arduino 复制代码
// OnPostTask的实现:将各个类型的任务转交给对应的TaskRunner处理
bool FlutterTaskExecutor::OnPostTask(Task&& task, TaskType type, uint32_t delayTime) const
{
    int32_t currentId = Container::CurrentId();
    auto traceIdFunc = [weak = WeakClaim(const_cast<FlutterTaskExecutor*>(this)), type]() {
        auto sp = weak.Upgrade();
        if (sp) {
            sp->taskIdTable_[static_cast<uint32_t>(type)]++;
        }
    };
    TaskExecutor::Task wrappedTask =
        currentId >= 0 ? WrapTaskWithContainer(std::move(task), currentId, std::move(traceIdFunc)) : std::move(task);
​
    switch (type) {
        case TaskType::PLATFORM:
            return PostTaskToTaskRunner(platformRunner_, std::move(wrappedTask), delayTime);
        case TaskType::UI:
            return PostTaskToTaskRunner(uiRunner_, std::move(wrappedTask), delayTime);
        case TaskType::IO:
            return PostTaskToTaskRunner(ioRunner_, std::move(wrappedTask), delayTime);
        case TaskType::GPU:
            return PostTaskToTaskRunner(gpuRunner_, std::move(wrappedTask), delayTime);
        case TaskType::JS:
            return PostTaskToTaskRunner(jsRunner_, std::move(wrappedTask), delayTime);
        case TaskType::BACKGROUND:
            // Ignore delay time
            return BackgroundTaskExecutor::GetInstance().PostTask(std::move(wrappedTask));
        default:
            return false;
    }
}
// PostTaskToTaskRunner的实现
bool PostTaskToTaskRunner(const fml::RefPtr<fml::TaskRunner>& taskRunner, TaskExecutor::Task&& task, uint32_t delayTime)
{
    CHECK_NULL_RETURN_NOLOG(taskRunner, false);
    CHECK_NULL_RETURN_NOLOG(task, false);
​
    if (delayTime > 0) {
        taskRunner->PostDelayedTask(std::move(task), fml::TimeDelta::FromMilliseconds(delayTime));
    } else {
        taskRunner->PostTask(std::move(task));
    }
    return true;
}

其中,比较特殊的是Background类型的任务会被交给后台任务执行器(BackgroundTaskExecutor)的单例对象去处理,具体的实现将在下一小节给出。

此处使用到的五种类型的fml::TaskRunner实例在创建的时候是有所区别的,具体如下。

  • platformRunner_的初始化:

platformRunner_是ArkUI框架中的主线程,相较于其他fml::TaskRunner实例比较特殊。

可以看到,platformRunner_是调用接口flutter::PlatformTaskRunner::CurrentTaskRunner(useCurrentEventRunner)返回的,返回值实际上是flutter::PlatformTaskRunnerAdapter::PlatformTaskRunnerAdapter类型的对象。该类通过对EventHandler和EventRunner的封装,继承并实现fml::TaskRunner 中与抛任务相关的接口。同时,flutter::PlatformTaskRunner::CurrentTaskRunner(useCurrentEventRunner)的入参为false,实际上是复用的元能力中创建的当前应用的主线程。

scss 复制代码
void FlutterTaskExecutor::InitPlatformThread(bool useCurrentEventRunner, bool isStageModel)
{
#if defined(OHOS_STANDARD_SYSTEM) || defined(PREVIEW)
    platformRunner_ = flutter::PlatformTaskRunner::CurrentTaskRunner(useCurrentEventRunner);
#else
#if defined(ANDROID_PLATFORM) || defined(IOS_PLATFORM)
    if (isStageModel) {
        LOGI("using eventhandler as platform thread in stage model.");
        platformRunner_ = flutter::PlatformTaskRunner::CurrentTaskRunner(useCurrentEventRunner);
    } else {
        LOGI("using messageLoop as platform thread in fa model.");
        fml::MessageLoop::EnsureInitializedForCurrentThread();
        platformRunner_ = fml::MessageLoop::GetCurrent().GetTaskRunner();
    }
#else
    fml::MessageLoop::EnsureInitializedForCurrentThread();
    platformRunner_ = fml::MessageLoop::GetCurrent().GetTaskRunner();
#endif
#endif
​
    FillTaskTypeTable(TaskType::PLATFORM);
}

注: flutter::PlatformTaskRunnerAdapter::PlatformTaskRunnerAdapter的实现在三方仓中的flutter中,具体代码目录如下:third_party/flutter/engine/flutter/shell/platform/ohos/platform_task_runner_adapter.h

  • uiRunner_ioRunner_gpuRunner_的初始化:

这三个实例在是在InitOtherThreads接口中进行初始化,是包含在flutter::TaskRunners形参中传入的。具体如下:

ini 复制代码
void FlutterTaskExecutor::InitOtherThreads(const flutter::TaskRunners& taskRunners)
{
    uiRunner_ = taskRunners.GetUITaskRunner();
    ioRunner_ = taskRunners.GetIOTaskRunner();
#ifdef FLUTTER_2_5
    gpuRunner_ = taskRunners.GetRasterTaskRunner();
#else
    gpuRunner_ = taskRunners.GetGPUTaskRunner();
#endif
​
    PostTaskToTaskRunner(
        uiRunner_, [] { SetThreadPriority(UI_THREAD_PRIORITY); }, 0);
    PostTaskToTaskRunner(
        gpuRunner_, [] { SetThreadPriority(GPU_THREAD_PRIORITY); }, 0);
​
    PostTaskToTaskRunner(
        uiRunner_, [weak = AceType::WeakClaim(this)] { FillTaskTypeTable(weak, TaskType::UI); }, 0);
    PostTaskToTaskRunner(
        ioRunner_, [weak = AceType::WeakClaim(this)] { FillTaskTypeTable(weak, TaskType::IO); }, 0);
    PostTaskToTaskRunner(
        gpuRunner_, [weak = AceType::WeakClaim(this)] { FillTaskTypeTable(weak, TaskType::GPU); }, 0);
}

调用InitOtherThreads接口传入的flutter::TaskRunners实例是通过FlutterThreadModel 类的CreateThreadModel方法创建的:

rust 复制代码
std::unique_ptr<FlutterThreadModel> FlutterThreadModel::CreateThreadModel(
    bool useCurrentEventRunner, bool hasUiThread, bool hasGpuThread)
{
    // Create threads
    static size_t sCount = 1;
    auto threadLabel = std::to_string(sCount++);
    uint64_t typeMask = 0;
​
    if (hasUiThread) {
        typeMask |= flutter::ThreadHost::Type::UI;
    }
    if (hasGpuThread) {
        typeMask |= flutter::ThreadHost::Type::GPU;
    }
    flutter::ThreadHost threadHost = { threadLabel, typeMask };
​
    // Create Taskrunners
    fml::MessageLoop::EnsureInitializedForCurrentThread();
    fml::RefPtr<fml::TaskRunner> gpuRunner;
    fml::RefPtr<fml::TaskRunner> uiRunner;
    
#if defined(OHOS_PLATFORM) || defined(PREVIEW)
    fml::RefPtr<fml::TaskRunner> platformRunner =
        flutter::PlatformTaskRunnerAdapter::CurrentTaskRunner(useCurrentEventRunner);
#else
#if defined(ANDROID_PLATFORM) || defined(IOS_PLATFORM)
    fml::RefPtr<fml::TaskRunner> platformRunner;
    if (hasUiThread) {
        platformRunner = fml::MessageLoop::GetCurrent().GetTaskRunner();
    } else {
        LOGI("FlutterThreadModel create platfrom thread by eventhandler.");
        platformRunner = flutter::PlatformTaskRunnerAdapter::CurrentTaskRunner(useCurrentEventRunner);
    }
#else
    fml::RefPtr<fml::TaskRunner> platformRunner;
    platformRunner = fml::MessageLoop::GetCurrent().GetTaskRunner();
#endif
#endif
​
    if (hasUiThread) {
        uiRunner = threadHost.ui_thread->GetTaskRunner();
    } else {
        uiRunner = platformRunner;
    }
    if (hasGpuThread) {
        gpuRunner = threadHost.gpu_thread->GetTaskRunner();
    } else {
        gpuRunner = uiRunner;
    }
​
    flutter::TaskRunners taskRunners(threadLabel, // label
        platformRunner,                           // platform
        gpuRunner,                                // gpu
        uiRunner,                                 // ui
        uiRunner                                  // io
    );
​
    return std::make_unique<FlutterThreadModel>(std::move(threadHost), std::move(taskRunners));
}

可以看到,该函数的第2、3个入参会指定是否启用新的线程执行UI和GPU类型的任务。可以看到:如果不启动新的线程执行UI类型任务,则UI类型任务会共用Platform类型任务的线程;如果不启动新的线程执行GPU类型任务,则GPU类型任务会共用UI类型任务的线程。另外,IO类型的任务会直接共用UI类型任务的线程。这些TaskRunner实例创建好后,会统一交给一个flutter::TaskRunners类的实例进行管理。

  • jsRunner_的初始化:

jsRunner_uiRunner_ioRunner_gpuRunner_并无本质不同,只是flutter使用的是Dart语言,没有js任务,所以在flutter::TaskRunners中没有包含jsRunner。jsRunner_的初始化由下边的代码给出:

ini 复制代码
void FlutterTaskExecutor::InitJsThread(bool newThread)
{
    if (newThread) {
        jsThread_ = std::make_unique<fml::Thread>(GenJsThreadName());
        jsRunner_ = jsThread_->GetTaskRunner();
    } else {
        jsRunner_ = uiRunner_;
    }
​
    PostTaskToTaskRunner(
        jsRunner_, [weak = AceType::WeakClaim(this)] { FillTaskTypeTable(weak, TaskType::JS); }, 0);
}

可以看到,在初始化jsRunner_时,会通过入参控制是否另起一个线程。

本小节给出了OnPostTask函数的具体实现,但是,其内部会调用到flutter::TaskRunner中的接口,这涉及到flutter线程库中的实现,基本原理与我们将在下一节介绍的后台任务执行器(BackgroundTaskExecutor)有相通之处。不同之处在于,flutter线程库中对任务队列的处理是基于epoll模型实现的。

2.2 后台任务执行器

可以看到,在上一小节的OnPostTask函数实现中Background类型的任务会被交给BackgroundTaskExecutor类的单例去处理。在本小节,我们对该类的实现进行介绍。

BackgroundTaskExecutor类实现了一个线程池,该类的实例会持有两个任务队列(对应普通优先级和低优先级)和一组(8个)线程,每个线程的执行体都是一个死循环,在没有任务的时候会处于休眠状态。同时,该类提供了向该线程池抛任务的接口,当抛任务的接口被执行时,对应的任务会被添加到相应的任务队列,同时,会唤醒一个线程去处理该任务。

关键属性

在BackgroundTaskExecutor类中,如下几个关键属性:

arduino 复制代码
std::mutex mutex_;
std::condition_variable condition_;
std::list<Task> tasks_;
std::list<Task> lowPriorityTasks_;
std::list<std::thread> threads_;
  • tasks_:普通优先级任务队列
  • lowPriorityTasks_:低优先级任务队列
  • mutex_:用于保护任务队列的锁
  • condition_:用于线程同步,线程池中的线程会调用wait方法阻塞当前线程,直到其他线程抛任务时调用notify_one方法唤醒线程池中的一个线程处理任务。
  • threads_:用于保存线程池中的所有线程。

关键方法

在BackgroundTaskExecutor中,我们关注如下三个接口的实现:PostTaskStartNewThreadsThreadLoop

  • StartNewThreads函数

在StartNewThreads函数中,主要关键程序如下,用于启动多个线程,并将ThreadLoop函数设置为每个线程的主函数。

c 复制代码
// Start new threads.
std::list<std::thread> newThreads;
for (size_t idx = 0; idx < num; ++idx) {
    newThreads.emplace_back(std::bind(&BackgroundTaskExecutor::ThreadLoop, this, currentThreadNo + idx));
}
  • ThreadLoop函数
scss 复制代码
void BackgroundTaskExecutor::ThreadLoop(uint32_t threadNo)
{
    LOGD("Background thread is started");
​
    SetThreadName(threadNo);
​
    Task task;
    const uint32_t purgeFlag = (1 << (threadNo - 1));
    std::unique_lock<std::mutex> lock(mutex_);
    while (running_) {
        if (tasks_.empty() && lowPriorityTasks_.empty()) {
            if ((purgeFlags_ & purgeFlag) != purgeFlag) {
                condition_.wait(lock);
                continue;
            }
​
            lock.unlock();
            LOGD("Purge malloc cache for background thread %{public}u", threadNo);
            PurgeMallocCache();
            lock.lock();
            purgeFlags_ &= ~purgeFlag;
            continue;
        }
        // deal with tasks_ first. do lowPriorityTasks_ only when all tasks_ done.
        if (!tasks_.empty()) {
            task = std::move(tasks_.front());
            tasks_.pop_front();
        } else {
            task = std::move(lowPriorityTasks_.front());
            lowPriorityTasks_.pop_front();
        }
​
        lock.unlock();
        // Execute the task and clear after execution.
        task();
        task = nullptr;
        lock.lock();
    }
​
    LOGD("Background thread is stopped");
}

可以看到,ThreadLoop函数中主要就是一个死循环。在循环体中,如果普通任务队列或低优先级任务队列不为空,则从任务队列中弹出任务并执行;若两个队列均为空,则通过condition_.wait(lock)阻塞当前线程。

  • PostTask函数

此函数针对左值引用和右值引用的任务入参,有两种形式,实现上近乎完全相同,我们只关注其中一个版本即可。具体实现如下:

arduino 复制代码
bool BackgroundTaskExecutor::PostTask(Task&& task, BgTaskPriority priority)
{
    if (!task) {
        return false;
    }
​
    std::lock_guard<std::mutex> lock(mutex_);
    if (!running_) {
        return false;
    }
    FrameTraceAdapter* ft = FrameTraceAdapter::GetInstance();
    if (ft != nullptr && ft->IsEnabled()) {
        switch (priority) {
            case BgTaskPriority::LOW:
                ft->QuickExecute(std::move(task));
                break;
            default:
                ft->SlowExecute(std::move(task));
                break;
        }
        return true;
    }
    switch (priority) {
        case BgTaskPriority::LOW:
            lowPriorityTasks_.emplace_back(std::move(task));
            break;
        default:
            tasks_.emplace_back(std::move(task));
            break;
    }
    condition_.notify_one();
    return true;
}

此函数中,我们不需要关注FrameTraceAdapter相关的内容。可以看出来,函数工作就是将任务加入到对应优先级的队列。并调用condition_.notify_one()唤醒线程池中的一个线程进行处理。

2.3 基本任务类型

CancelableCallback<void(V...)>::Cancel(bool waitUntilCompleted):

  • 若任务的当前状态为READY,则将状态设置为CANCLED,并不再执行任务,返回true;
  • 若任务的当前状态为CANCLED,表明任务未执行,且不再执行,返回true;
  • 若任务的当前状态为RUNNING,等待任务执行完毕,并将任务状态设置为COMPLETED,返回false;
  • 若任务的当前状态为COMPLETED,则表明任务已完成,返回false;

2.4 任务执行器的创建流程

3. fml::TaskRunner

background_task_executor.h

复制代码
    bool PostTaskAndWait(CancelableTask&& task, TaskType type, std::chrono::milliseconds timeoutMs = 0ms) const
    {
        return OnPostTask(Task(task), type, 0) && task.WaitUntilComplete(timeoutMs);
    }
    
相关推荐
一路向前的月光3 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   3 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web3 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
Jiaberrr4 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
安冬的码畜日常6 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ6 小时前
html+css+js实现step进度条效果
javascript·css·html
john_hjy6 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd7 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
yanlele7 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范