.NET 除了用 Task 之外,如何自己写一个可以 await 的对象?

.NET 中的 async / await 写异步代码用起来真的很爽,就像写同步一样。我们可以在各种各样的异步代码中看到 Task 返回值,这样大家便可以使用 await 等待这个方法。不过,有时需要写一些特别的异步方法,这时需要自己来实现一个可以异步等待的对象。

本文将讲述如何实现一个可等待对象,一个自定义的 Awaiter。


Awaiter 系列文章

入门篇:

实战篇:

可等待对象

我们希望大家在调用下面的 CallWalterlvAsync 方法的时候,可以使用 await 关键字来异步等待:

await CallWalterlvAsync();
public WalterlvOperation CallWalterlvAsync()
{
    // 返回一个 WalterlvOperation,以便外面调用方可以异步等待。
    return new WalterlvOperation();
}

所以我们需要实现一个 WalterlvOperation

编写基本的 Awaiter 框架代码

先写一个空的类型,然后为它编写一个空的 GetAwaiter 方法,返回新的 WalterlvAwaiter 类型。

/// <summary>
/// 委托 walterlv 来完成一项特殊的任务。
/// 通过在代码当中调用,可以让他在现实中为你做一些事情。
/// </summary>
public class WalterlvOperation
{
    public WalterlvAwaiter GetAwaiter()
    {
        return new WalterlvAwaiter();
    }
}

接着,我们编写 WalterlvAwaiter 类:

public class WalterlvAwaiter : INotifyCompletion
{
    public bool IsCompleted { get; }
    public void GetResult() { }
    public void OnCompleted(Action continuation) { }
}

必须实现 INotifyCompletion 接口,此接口带来了 OnCompleted 方法。另外两个方法不是接口带来的,但是也是实现一个自定义的 Awaiter 必要的方法。

在你编写完以上两段代码之后,你的 await 就可以编译通过了。

额外说明一下,GetResult 方法是可以修改返回值的,只要返回值不是 void,那么 await 等待的地方将可以在 await 完成之后获得一个返回值。

public class WalterlvAwaiter : INotifyCompletion
{
    public bool IsCompleted { get; }
    public string GetResult() { }
    public void OnCompleted(Action continuation) { }
}
// 于是你可以拿到一个字符串类型的返回值。
string result = await CallWalterlvAsync("写博客");

实现基本的 Awaiter

以上代码只能编译通过,但实际上如果你跑起来,会发现 await 一旦进入,是不会再往下执行的。因为我们还没有实现 WalterlvAwaiter 类型。

最重要的,是需要调用 OnCompleted 方法传入的 continuation 委托。

public void OnCompleted(Action continuation)
{
    continuation.Invoke();
}

像以上这么写之后,await 之后的代码便可以执行了。

如果你只是希望了解如何实现一个 Awaiter,那么写出以上的代码就足以。因为这才是最本质最核心的 Awaiter 的实现。

不过,以上代码的执行是立即执行,没有任何异步的效果。因为 OnCompleted 被调用的时候,我们立刻调用了 continuation 的执行。

实现异步的 Awaiter

要真正达到异步的效果,OnCompleted 执行的时候,我们不能立刻去调用参数传进来的委托,而只是将他记录下来,等到任务真正完成的时候再去调用。

以下的代码就不再是通用的代码了,你需要针对你的不同业务去设计如何异步完成一个任务,然后再通知到异步等待的代码继续执行。

例如,现在我们期望 walterlv 代理去写博客,于是我们为 WalterlvOperation 加一点功能,真正去做一些异步的事情。

CallWalterlvAsync 的实现现在真的开启了一个异步操作。

public WalterlvOperation CallWalterlvAsync(string task)
{
    var operation = new WalterlvOperation(task);
    operation.Start();
    return operation;
}

然后为了实现我们自己添加的 Start 方法,我们在里面去做一些事情。里面第一句就离开了当前线程前往线程池中的其他线程去执行 Console.WriteLine 了。

/// <summary>
/// 委托 walterlv 来完成一项特殊的任务。
/// 通过在代码当中调用,可以让他在现实中为你做一些事情。
/// </summary>
public class WalterlvOperation
{
    private readonly string _task;
    private readonly WalterlvAwaiter _awaiter;

    public WalterlvOperation(string task)
    {
        _task = task;
        _awaiter = new WalterlvAwaiter();
    }

    public async void Start()
    {
        await Task.Delay(100).ConfigureAwait(false);
        Console.WriteLine($"walterlv 已经收到任务:{_task}");
        Console.WriteLine($"开始执行");
        await Task.Delay(2000).ConfigureAwait(false);
        Console.WriteLine($"walterlv 已经完成 {_task}。");
        _awaiter.ReportCompleted();
    }

    /// <summary>
    /// 返回一个可等待对象,以便能够使用 await 关键字进行异步等待。
    /// </summary>
    public WalterlvAwaiter GetAwaiter()
    {
        return _awaiter;
    }
}

于是现在可以通过下面的代码来要求 walterlv 去写博客了。

await CallWalterlvAsync("写博客");

然而实际上,我们上面还留了一个 _awaiter.ReportCompleted 方法没有实现。由于我们的操作全部是异步的了,这个方法的实现就是为了通知所有正在使用 await 等待的代码,异步任务完成了,可以继续往后面执行了。

public class WalterlvAwaiter : INotifyCompletion
{
    private Action _continuation;
    public bool IsCompleted { get; private set; }

    public void GetResult()
    {
        // 这个函数我们暂时还没有真正实现,因为需要进行同步等待比较复杂。
        // 我们将在本文后面附的其他博客中实现。
    }

    public void OnCompleted(Action continuation)
    {
        // 当这个 Awaiter 被 await 等待的时候,此代码会被调用。
        // 每有一处 await 执行到,这里就会执行一次,所以在任务完成之前我们需要 +=。
        if (IsCompleted) 
        {
            continuation?.Invoke();
        }
        else
        {
            _continuation += continuation;
        }
    }

    public void ReportCompleted()
    {
        // 由 WalterlvOperation 来通知这个任务已经完成。
        IsCompleted = true;
        var continuation = _continuation;
        _continuation = null;
        continuation?.Invoke();
    }
}

现在运行程序,会按照异步任务来执行,可以异步等待:

static async Task Main(string[] args)
{
    await CallWalterlvAsync("写博客");
    Console.Read();
}

程序运行结果

本文会经常更新,请阅读原文: https://walterlv.com/post/understand-and-write-custom-awaiter.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com)