MozPromise:Gecko 中的 C++ Promise

MozPromise 是一个功能强大且灵活的 C++ Promise 实现,旨在管理 Gecko 中的异步操作。其功能集反映了 JavaScript Promise 的精神,包括附加解决和拒绝回调、链接 Promise 以及跨不同线程处理异步任务的能力。 MozPromise 支持排他性和非排他性 Promise。排他性 Promise 强制 Promise 最多只能调用一次解决或拒绝回调,确保 Promise 以可预测且可控的方式使用。另一方面,非排他性 Promise 允许多次解决或拒绝操作,为更复杂的用例提供灵活性。

此外,MozPromise 提供了断开 Promise 的机制,允许使用者在不再需要时取消解决或拒绝值的传递。这些功能使 MozPromise 成为管理跨多个线程的异步工作流、确保线程安全并在复杂应用程序中维护操作正确顺序的多功能工具,这与 Gecko 代码库中通常看到的模式形成对比,该模式是手动调度 Runnable

跨线程同步状态的另一种选择是使用 状态镜像

MozPromise 与 DOM Promise 类没有真正的关系,但通常可以结合使用。例如,通过从传递给 MozPromiseThen(...) 的 lambda 中调用 dom::Promise::MaybeResolve 在主线程上完成此操作。

在本文档中,**生产者**是创建、然后解决或拒绝 Promise 的代码部分,并且通常启动或执行作为异步操作一部分需要完成的工作。**消费者**是将对 Promise 被解决或拒绝做出反应的代码部分,因此能够了解工作的完成情况。

MozPromise 的保证

MozPromise 提供了几项保证,以确保可预测且可靠的行为。这些包括

  • **顺序**:MozPromise 确保解决或拒绝回调按附加顺序调度,维护操作的正确顺序。在 Promise 完成后附加回调将调用它们。

  • **线程安全**:它们被设计为线程安全的,允许 Promise 在不同的线程上创建、链接、解决和拒绝,而不会导致竞争条件。解决/拒绝处理程序始终在其目标线程上销毁。但是,MozPromiseHolderMozPromiseRequestHolder 本身需要同步。

  • **完成**:一旦 Promise 被解决或拒绝,它就不能被更改,确保 Promise 的状态一致且不可变。 *Holder 实例可以重复使用。

典型用法

如果工作负载是同步的

从生产者方面
  • 完成工作

  • 通过 MozPromise::CreateAndResolveMozPromise::CreateAndReject 返回已解决或已拒绝的 Promise

从消费者方面
  • 调用返回 Promise 的方法

  • 在 Promise 上调用 Then() 以设置在 Promise 稳定后在给定线程上运行的操作。

如果工作负载是异步的

从生产者方面:- 分配一个 MozPromise(可能通过 MozPromiseHolder)并将其作为 RefPtr<MozPromise> 返回给消费者 - 使用 InvokeAsync 将异步工作调度到任何线程,并将返回的 Promise 转发给调用者。 - 工作完成后,使用 MozPromise::CreateAndResolveMozPromise::CreateAndReject 在异步任务中直接返回其结果。

从消费者方面:- 调用返回 MozPromise 的方法 - 在 Promise 上调用 Then() 以设置在 Promise 稳定后在给定线程上运行的操作。

在这两种情况下,都可以 Track() Promise,以取消解决/拒绝结果的传递并防止运行回调。

概念

Promise 运行的线程

Mozilla 的 MozPromise 框架中的 Promise 可以运行在各种线程上,具体取决于它们创建和解决的上下文。 Then 方法允许指定应在其中执行解决或拒绝回调的目标线程。 InvokeAsync 允许选择工作将发生在哪个线程上,通常这也是 Promise 被拒绝或解决的地方。

The Then(...) 方法

调用此方法以设置在 Promise 稳定后要调用的拒绝/解决回调。有两种指定它们的方法。第一种样式是传递单个回调,使用类型 MyPromise::ResolveOrRejectValue(如果 MyPromise 已被别名为特定类型),并通过检查值本身来处理拒绝/解决

RefPtr<MyPromise> promise = /* from somehere */

promise->Then(mainThread, __func__,
    [this, self = RefPtr{this}]
    (const MyPromise::ResolveOrRejectValue& aValue) {
    if (aValue.IsResolve())) {
        /* HandleResolvedValue(value); */
        return;
    }
    /* HandleRejectedValue(); */
    });

另一种样式是传递两个回调,一个用于解决(第一个参数),一个用于拒绝(第二个参数)

RefPtr<MyPromise> promise = /* from somewhere */

// Granted those functions have the correct parameters types.
promise->Then(
    mainThread, __func__, &HandleResolvedValue, &HandleRejectedValue);

在这些回调中使用的确切参数类型取决于 Promise 的排他性,请参阅下面的部分。

当传递方法指针时,捕获列表中需要一个引用计数实例指针。可以使用函数指针和 lambda。

Then(…) 方法返回一个对象,该对象可用于执行两件事

  • 将其转换回 MozPromise,该 Promise 将在第一个 Promise 的解决/拒绝被调用后解决。这允许通过依次在该转换后的 Promise 上调用 Then(...) 来链接多个 Promise。

  • 跟踪第一个 Promise,以便能够取消回调的传递,如果它们尚未被调用。这是通过断开 MozPromiseRequestHolder 来完成的。

引用计数

由于 Promise 本质上是异步的,并且可以在各种线程上运行,因此必须确保将在回调函数中使用的对象在 Promise 被拒绝或解决时仍然有效。这通常通过向类添加引用计数,并将 this 指针的 addrefed 副本传递到 lambda 中来完成,如下所示

class SomeClass {
public:
    // Adding refcounting to a class:
    NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SomeClass)

    RefPtr<MyPromise> DoIt() {
        RefPtr<MyPromise> promise = mHolder.Ensure(__func__);

        promise->Then(
            backgroundThread, __func__,
            // Make the lambda has keeping a reference to the class via
            // the capture list, by creating a new RefPtr in the lambda's
            // scope
            [this, self = RefPtr{this}](int value) {
                /* handle resolution */
            },
            // Reject and resolve have the same lifetime, no need
            // to do anything here if we don't need a reference to this
            // (e.g. we're just logging, etc.)
            [](nsresult error) { /* HandleRejectedValue(error); */ });

        return promise.forget();
    }
private:
    MozPromiseHolder<MyPromise> mHolder;
};

排他性

MozPromise 中的排他性是指强制 Promise 解决或拒绝为单个回调集的能力。当 IsExclusive 模板参数设置为 true 时,Promise 会阻止在单个 Promise 上进行多次解决或拒绝回调,当此功能对于特定用途并非必需时,例如当这可能导致意外行为时。

此不变式通过断言进行检查,该断言在发布版本中启用,并且在尝试在排他性 Promise 上安装第二组回调时将失败。

当 Promise 是排他性的时,结果值使用右值引用移动到解决回调中。因此,典型的签名是

using MyPromise = MozPromise<int, nsresult, true>;
void Callback(MyPromise::ResolveOrRejectValue&& aResult);

这意味着回调是该值的唯一所有者。回调的闭包将在它们被调用的线程上删除。因此,用于拒绝/解决值的类型需要可移动或可复制。

但是,如果 Promise 不是排他性的,则结果使用 const 左值引用传递

void Callback(const MyPromise::ResolveOrRejectValue& aResult);

这允许多个回调引用该值。

主要类

MozPromise

MozPromise 是一个表示 C++ 中 Promise 的模板类,类似于 JavaScript Promise。它管理一个可能无法立即满足的异步请求。 MozPromise 的模板参数是

  • ResolveValueT:Promise 解析到的值的类型。

  • RejectValueT:Promise 拒绝时的值的类型。

  • IsExclusive:一个布尔标志,指示 Promise 是否为排他性,

    这意味着它只能解决或拒绝一次。

使用 typedefusing 为 Promise 类型设置别名是一种常见的做法,可以简化使用具有特定解决和拒绝类型的 MozPromise。例如

using CustomBoolPromise = MozPromise<bool, nsresult, true>;

定义了一种通用的排他性 Promise 类型,它解析为布尔值,并以nsresult拒绝。

这使得代码更易读且更易于维护,因为 Promise 的具体类型已明确定义,并且可以在整个代码库中重复使用。

MozPromiseHolder

MozPromiseHolder是一个模板类,旨在封装一个MozPromise。它对于方法返回 Promise 的类很有用,即异步请求的“内部”:最终将解析或拒绝的部分。它适用于高级情况,在这些情况下InvokeAsync 不够。

通常,您将MozPromiseHolder存储在将向调用者返回 Promise 并内部解析这些 Promise 的类中。为了确保安全,MozPromiseHolder不应泄漏到其拥有者类或嵌套类之外,就像 JS Promise 的 resolve/reject 函数不应泄漏到构造函数作用域之外一样。

MozPromiseHolder提供方法来确保创建 Promise,检查它是否为空,窃取私有 Promise,解析或拒绝 Promise,以及设置任务调度和优先级。它允许在类**内部**管理 Promise,确保正确处理 Promise,并可以根据需要解析或拒绝。请注意,MozPromiseHolder本身不是线程安全的,尽管它封装的 Promise 是线程安全的。

class SomeClass {
public:
    RefPtr<MyPromise> DoIt() {
        RefPtr<MyPromise> promise = mHolder.Ensure(__func__);
        MOZ_ASSERT(!mHolder.IsEmpty());

        // ... deep inside some async code, potentially on a different thread,
        // resolve the promise via the holder:
        // mHolder.Resolve(42, __func__);
        // It is empty after resolving
        // MOZ_ASSERT(mHolder.IsEmpty());

        return promise.forget();
    }
private:
    MozPromiseHolder<MyPromise> mHolder;
};

MozPromise::Request / MozPromiseRequestHolder

MozPromiseRequestHolder是一个模板类,它封装了一个MozPromise::Request引用,很少直接使用。它由可能想要断开等待MozPromise的类的类使用,即异步请求的“外部”。此类提供方法来跟踪请求、完成请求、断开请求以及检查请求是否存在。它对于管理 Promise 请求的生命周期很有用,确保可以根据需要正确跟踪、完成或断开请求。

本质上,这是在MozPromise框架内进行的特定请求的句柄。

断开请求**必须**在它正在跟踪的 resolve/reject 处理程序的目标线程上发生。当调用Disconnect()时,此处理程序将被释放。

在处理接近 WebIDL 绑定层的MozPromise时,另一个选项是DOMMozPromiseRequestHolder,它将在全局消失时适当地断开 Promise。否则它的工作方式相同。

要将MozPromiseRequestHolderMozPromise关联,可以使用Track(...)方法。

class SomeClass {
public:
    // refcounting is mandatory
    NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SomeClass)
    RefPtr<MyPromise> DoIt() {
        RefPtr<MyPromise> promise = mHolder.Ensure(__func__);
        MOZ_ASSERT(!mHolder.IsEmpty());

        promise->Then(
            backgroundThread, __func__,
            [this, self = RefPtr{this}](int value) {
              // Resolved: mark as complete
              mRequestHandle.Complete();
              /* do something with value */
            },
            [](nsresult error) {
              // Rejected: also mark as complete
              mRequestHandle.Complete();
              /* HandleRejectedValue(error); */
        }).Track(mRequestHandle);

        // ... deep inside some async code, potentially on a different thread,
        // resolve the promise:
        // promise.Resolve(42, __func__);

        return promise.forget();
    }
    void CancelIt() {
        // Functions passed to Then() won't be called. This must
        // be called on `backgroundThread`
        mRequestHandle.DisconnectIfExists();
    }
private:
    MozPromiseHolder<MyPromise> mHolder;
    MozPromiseRequestHolder<MyPromise> mRequestHandle;
};

InvokeAsync 函数

InvokeAsync函数用于在给定线程上异步调用返回 Promise 的函数。它分派一个任务以在正确的线程上调用该函数,并将结果 Promise 与调用者收到的 Promise 链接起来,以便转发 resolve/reject 值。此函数对于调度返回 Promise 的异步任务很有用,确保任务在正确的线程上执行,并且 Promise 正确链接。

class SomeClass {
    public:
    NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SomeClass)
    RefPtr<MyPromise> AsyncFunction(nsISerialEventTarget* target) {
        return InvokeAsync(target, __func__, []() -> RefPtr<MyPromise> {
            // ... some expensive async work is happening
            int result = 42;
            return MyPromise::CreateAndResolve(result, __func__);
        });
    }

    RefPtr<MyPromise> DoItAsync() {
        nsCOMPtr<nsISerialEventTarget> backgroundThread = /* from somewhere */;
        nsCOMPtr<nsISerialEventTarget> mainThread = do_GetMainThread();

        // Call the async function on the background task queue
        RefPtr<MyPromise> promise = AsyncFunction(backgroundThread);

        // But get the completion callbacks on the main thread
        promise->Then(
            mainThread, __func__,
            [this, self = RefPtr{this}](int value) {
              /* HandleResolvedValue(value); */
            },
            [](nsresult error) {
              /* HandleRejectedValue(error); */
        });

        return promise.forget());
    }
};

高级特性

直接任务调度

直接任务调度MozPromise中的一项功能,它允许 resolve 或 reject 回调在直接任务队列上执行,而不是在正常的事件循环上执行。这对于涉及多个异步步骤的场景特别有用,因为它避免了每次额外的异步步骤都完全回到事件队列末尾。通过使用直接任务调度,回调会更及时地执行,从而减少延迟并提高应用程序的整体响应能力。

只有当回调设置为在调用者所在的同一线程上运行时,此功能才可用。

在 Web 领域,这类似于在微任务检查点而不是常规事件循环任务中执行某些操作。虽然它是 Web Promise 的默认设置,但在MozPromise中是可选的。

要启用直接任务调度,请在MozPromiseHolder实例上调用UseDirectTaskDispatch方法。此方法将 Promise 设置为使用直接事件队列来分派 resolve 或 reject 回调。

一个相关的概念是Runnable“尾部调度”

同步调度

同步调度是 MozPromise 中的另一个功能,它允许 resolve 或 reject 回调在同一线程上同步执行,而不是异步分派。这在回调需要立即执行而无需等待事件循环处理它们的场景中很有用。同步调度确保回调以可预测和及时的方式执行,这对于某些类型的操作至关重要。

只有当回调设置为在调用者所在的同一线程上运行时,此功能才可用。

要启用同步调度,请在 MozPromiseHolder 实例上调用 UseSynchronousTaskDispatch 方法。此方法将 Promise 设置为在同一线程上同步执行 resolve 或 reject 回调。当 Promise 解析或拒绝时,回调会立即执行,而不会分派到事件循环。

但是,同步调度可能会引入潜在问题,例如死锁。当两个或多个线程都在等待彼此释放资源时,就会发生死锁,导致任何线程都无法继续执行。在 MozPromise 的上下文中,如果 resolve 或 reject 回调正在等待同一线程持有的资源,则可能会发生死锁,导致线程无限期阻塞。

为了减轻死锁的风险,务必谨慎使用同步调度,并确保回调不依赖于同一线程持有的资源。

注意事项

销毁尚未解析或拒绝的 Promise 是错误的。因此,在这种情况下去销毁拥有MozPromiseHolder的对象会断言。

在处理MozPromise(与大多数异步构造一样)时,关闭阶段可能是一个问题。由于无法处理无法分派到线程的错误,因此将 Promise 链设置为在可能已关闭的线程上运行某些处理程序是错误的。解决此问题的一种方法是提供线程保证,通过阻止关闭,或者在关闭时通过MozPromiseRequestHolder断开 Promise。两种方法都可能需要。

当使用MozPromiseHolder::Ensure时,即使之前的 Promise 已经完成,也会创建一个新的MozPromise。有时需要外部簿记(例如保留MozPromise以检查它是否相同)以确保处理程序设置在正确的MozPromise上,而不是潜在的另一个 Promise 上。