【深入浅出C# async/await】理解 awaitable-awaiter 模式

Part1 - 【深入浅出C# async/await】编译篇
Part2 - 【深入浅出C# async/await】理解 awaitable-awaiter 模式

什么是awaitable

Part1中我们展示了任何Task都是可等待的。实际上还有其他可等待的类型。看下面的例子:

Task<int> task = new Task<int>(() => 0);
int result = await task.ConfigureAwait(false); //返回 ConfiguredTaskAwaitable<TResult>.

返回的 结构体 ConfiguredTaskAwaitable是可等待的,并且这完全不是Task:

public struct ConfiguredTaskAwaitable<TResult>
{
    private readonly ConfiguredTaskAwaiter m_configuredTaskAwaiter;

    internal ConfiguredTaskAwaitable(Task<TResult> task, bool continueOnCapturedContext)
    {
        this.m_configuredTaskAwaiter = new ConfiguredTaskAwaiter(task, continueOnCapturedContext);
    }

    public ConfiguredTaskAwaiter GetAwaiter()
    {
        return this.m_configuredTaskAwaiter;
    }
}

它拥有一个GetAwaiter方法,实际上在Part1我们在Task中也看到了GetAwaiter()。

public class Task
{
    public TaskAwaiter GetAwaiter()
    {
        return new TaskAwaiter(this);
    }
}

public class Task<TResult> : Task
{
    public new TaskAwaiter<TResult> GetAwaiter()
    {
        return new TaskAwaiter<TResult>(this);
    }
}

另外一个例子是Task.Yield():

await Task.Yield(); // Returns a YieldAwaitable.

返回的YieldAwaitable也不是一个Task:

public struct YieldAwaitable
{
    public YieldAwaiter GetAwaiter()
    {
        return default(YieldAwaiter);
    }
}

同样,它只有一个GetAwaiter(),本文将主要探讨什么是可等待的。

awaitable - awaiter模式

观察不同的 awaitable/awaiter类型,我们可以发现,如果一个对象满足以下套件,那么它是是可等待的:

  • 拥有一个GetAwaiter()方法(实例方法或者拓展方法都可)
  • GetAwaiter()方法返回awaiter。当一个对象符合以下条件,那么它是一个awaiter:
    • 继承了INotifyCompletion或者ICriticalNotifyCompletion接口;
    • 拥有成员IsCompleted,IsCompleted有一个getter并且返回Boolean;
    • 拥有GetResult(),返回void或者result;
      这个 可等待/等待器模式 和 可迭代/迭代器模式 十分相似。下面是 可迭代/迭代器 接口的定义:
public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

public interface IEnumerator
{
    object Current { get; }

    bool MoveNext();

    void Reset();
}

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

//此处out T表示T只能作为方法返回类型,不能用于方法参数
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
    T Current { get; }
}

"消失"的 IAwaitable和IAwaiter接口

和 IEnumarable 和 IEnumerator接口相同,awaitable/awaiter可以被可视化为IAwaitable/IAwaiter接口,这是非泛型版本:

public interface IAwaitable
{
    IAwaiter GetAwaiter();
}

public interface IAwaiter : INotifyCompletion // or ICriticalNotifyCompletion
{
    // INotifyCompletion has one method: void OnCompleted(Action continuation);

    // ICriticalNotifyCompletion implements INotifyCompletion,
    // also has this method: void UnsafeOnCompleted(Action continuation);

    bool IsCompleted { get; } //Task执行完毕

    void GetResult(); //Task执行完,获取结果,因为是非泛型结果,所以返回值为void
}

请注意GetResult()返回void,Task.GetAwaiter/TaskAwaiter.GetResult()符合这种情况。
下面是泛型版本:

public interface IAwaitable<out TResult>
{
    IAwaiter<TResult> GetAwaiter();
}

public interface IAwaiter<out TResult> : INotifyCompletion // or ICriticalNotifyCompletion
{
    bool IsCompleted { get; }

    TResult GetResult();
}

这里唯一的区别就是GetResult()返回Result,Task.GetAwaiter()/TaskAwaiter.GetResult()符合这种情况。

请注意 .NET core实际上并没有定义IAwaitable/IAwaiter接口。IAwaitable接口会约束实例必须拥有GetAwaiter()方法。实际上C#支持GetAwaiter()实例方法和GetAwaiter拓展方法。

等待 Func/Action

在C#中我们并不能await并不能和lambda表达式一起用,下面的代码:

int result = await (() => 0);

编译器中会报错。

Cannot await ‘lambda expression’

这很好理解,因为lambda表达式(()=>0)可能是一个function或者一颗表达树。显然这里是function,我们可以这样告诉编译器这里的lambda的含义:

int result = await new Func<int>(()=>0);

它会报一个不同的错。

Cannot await ‘System.Func<int>’

ok,现在编译器的警告是关于类型而不是语法,理解了awaitable/awaiter模式,让Func变得可被等待并不是一件难事。

继承IAwaitable和IAwaiter接口实现GetAwaiter实例方法

首先,类似于上面的 ConfiguredTaskAwaitable,可以实现一个 FuncAwaitable 来包装 Func:

internal struct FuncAwaitable<TResult> : IAwaitable<TResult>
{
    private readonly Func<TResult> function;

    public FuncAwaitable(Func<TResult> function)
    {
        this.function = function;
    }

    public IAwaiter<TResult> GetAwaiter()
    {
        return new FuncAwaiter<TResult>(this.function);
    }
}

FuncAwaitable 包装器用于实现 IAwaitable,因此它有一个实例方法 GetAwaiter(),该方法返回一个 IAwaiter,该 IAwaiter 也包装了 Func。FuncAwaiter 用于实现 IAwaiter:

public struct FuncAwaiter<TResult> : IAwaiter<TResult>
{
    private readonly Task<TResult> task;

    public FuncAwaiter(Func<TResult> function)
    {
        this.task = new Task<TResult>(function);
        this.task.Start();
    }

    bool IAwaiter<TResult>.IsCompleted
    {
        get
        {
            return this.task.IsCompleted;
        }
    }

    TResult IAwaiter<TResult>.GetResult()
    {
        return this.task.Result;
    }
	//continuation可以看Part1的what is callback部分
    void INotifyCompletion.OnCompleted(Action continuation)
    {
    	//await之后的代码作为continuation传入
    	//当前task执行完毕后,开启一个新task执行continuation
        new Task(continuation).Start(); 
    }
}

现在一个Func可以通过这种方式被等待。

int result = await new FuncAwaitable<int>(() => 0);

GetAwaiter()静态方法,不继承IAwaitable接口

正如 IAwaitable 所示,可等待对象只需要一个 GetAwaiter() 方法。在上面的代码中,FuncAwaitable 被创建为 Func 的包装器,并实现了 IAwaitable 接口,因此有一个 GetAwaiter() 实例方法。如果可以为 Func 定义 GetAwaiter() 扩展方法,则不再需要 FuncAwaitable:

public static class FuncExtensions
{
    public static IAwaiter<TResult> GetAwaiter<TResult>(this Func<TResult> function)
    {
        return new FuncAwaiter<TResult>(function);
    }
}

可以看到现在Func是可以直接被等待的。

int result = await new Func<int>(() => 0);

使用内置的awaiter awaitable: Task和TaskAwaiter

请记住,最常用的可等待程序/等待程序 - Task / TaskAwaiter。使用 Task / TaskAwaiter,就不再需要 FuncAwaitable / FuncAwaiter:

public static class FuncExtensions
{
    public static TaskAwaiter<TResult> GetAwaiter<TResult>(this Func<TResult> function)
    {
        Task<TResult> task = new Task<TResult>(function);
        task.Start();
        return task.GetAwaiter(); // Returns a TaskAwaiter<TResult>.
    }
}

同样地,Action的拓展方法:

public static class ActionExtensions
{
    public static TaskAwaiter GetAwaiter(this Action action)
    {
        Task task = new Task(action);
        task.Start();
        return task.GetAwaiter(); // Returns a TaskAwaiter.
    }
}

action也能被等待了:

await new Action(() => { }); //记得不能直接 await ()=>{} 编译器不知道这个lambda表达式是委托还是语法

现在任何function/action都能被等待了:

await new Action(() => HelperMethods.IO()); // or: await new Action(HelperMethods.IO);

如果委托有参数,会导致闭包:

int arg0 = 0;
int arg1 = 1;
int result = await new Action(() => HelperMethods.IO(arg0, arg1));

使用Task.Run()

上面的代码用于演示如何实现可等待程序/等待程序。由于等待函数/操作是常见的场景,.NET 提供了内置 API:Task.Run()。它们的实现方式类似于:

public class Task
{
    public static Task Run(Action action)
    {
        // The implementation is similar to:
        Task task = new Task(action);
        task.Start();
        return task; //task有GetAwaiter()方法,这就是为啥Task可以被await
    }

    public static Task<TResult> Run<TResult>(Func<TResult> function)
    {
        // The implementation is similar to:
        Task<TResult> task = new Task<TResult>(function);
        task.Start();
        return task;
    }
}

这是我们实际上等待function的方式:

int result = await Task.Run(() => HelperMethods.IO(arg0, arg1));

等待action:

await Task.Run(HelperMethods.IO);

Await IObservable

如果添加了 System.Reactive.Linq.dll 的引用(Rx(Reactive Extensions)的一部分),IObservable 和 IConnectableObservable 也可以变成可等待程序。在此库中,提供了 GetAwaiter() 扩展方法:

public static class Observable
{
    public static AsyncSubject<TSource> GetAwaiter<TSource>(this IObservable<TSource> source);

    public static AsyncSubject<TSource> GetAwaiter<TSource>(this IConnectableObservable<TSource> source);
}

所有返回AsyncSubject的方法是迭代器:(C#8.0中的AsyncSubject,下面的例子看看就好)

public sealed class AsyncSubject<T> : INotifyCompletion, ISubject<T>, ISubject<T, T>, IObserver<T>, IObservable<T>, IDisposable
{
    public bool IsCompleted { get; }
    
    public void OnCompleted();

    // ...
}

所以它可以使用await关键字,举个IObservable的例子:

private static async Task AwaitObservable1()
{
    IObservable<int> observable = Observable.Range(0, 3).Do(Console.WriteLine);
    await observable;
}

输出

0
1
2

其他例子:

private static async Task<string> AwaitObservable2()
{
    IObservable<string> observable = new string[]
        {
            "https://weblogs.asp.net/dixin/understanding-c-sharp-async-await-1-compilation",
            "https://weblogs.asp.net/dixin/understanding-c-sharp-async-await-2-awaitable-awaiter-pattern",
            "https://weblogs.asp.net/dixin/understanding-c-sharp-async-await-3-runtime-context",
        }
        .ToObservable<string>()
        .SelectMany(async url => await new WebClient().DownloadStringTaskAsync(url))
        .Select(StringExtensions.GetTitleFromHtml)
        .Do(Console.WriteLine);

    return await observable;
}

当GetTitleFromHtml如下:

public static string GetTitleFromHtml(this string html)
{
    Match match = new Regex(
        @".*<head>.*<title>(.*)</title>.*</head>.*",
        RegexOptions.IgnoreCase | RegexOptions.Singleline).Match(html);
    return match.Success ? match.Groups[1].Value : null;
}

运行上面的AwaitObservable2会输出每页的标题:

Dixin's Blog - Understanding C# async / await (1) Compilation
Dixin's Blog - Understanding C# async / await (3) Runtime Context
Dixin's Blog - Understanding C# async / await (2) The Awaitable-Awaiter Pattern

确切地说他们是<title>和</title>之间的内容。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值