关于c#:asyncawait-在-C-语言中是如何工作的上

53次阅读

共计 15188 个字符,预计需要花费 38 分钟才能阅读完成。

前不久,咱们公布了《抉择 .NET 的 n 个理由》。它提供了对平台的高层次概述,总结了各种组件和设计决策,并承诺对所波及的畛域发表更深刻的文章。这是第一篇这样深入探讨 C# 和 .NET 中 async/await 的历史、背地的设计决策和实现细节的文章。

对 async/await 的反对曾经存在了十年之久。在这段时间里,它扭转了为 .NET 编写可扩大代码的形式,而在不理解其底层逻辑的状况下应用该性能是可行的,也是十分常见的。在这篇文章中,咱们将深入探讨 await 在语言、编译器和库级别的工作原理,以便你能够充分利用这些有价值的性能。

不过,要做到这一点,咱们须要追溯到 async/await 之前,以理解在没有它的状况下最先进的异步代码是什么样子的。

最后的样子

早在 .NET Framework 1.0 中,就有异步编程模型模式,又称 APM 模式、Begin/End 模式、IAsyncResult 模式。在高层次上,该模式很简略。对于同步操作 DoStuff:

class Handler
{public int DoStuff(string arg);
}

作为模式的一部分,将有两个相应的办法:BeginDoStuff 办法和 EndDoStuff 办法:

class Handler
{public int DoStuff(string arg);
    public IAsyncResult BeginDoStuff(string arg, AsyncCallback? callback, object? state);
    public int EndDoStuff(IAsyncResult asyncResult);
}

BeginDoStuff 会像 DoStuff 一样承受所有雷同的参数,但除此之外,它还会承受 AsyncCallback 委托和一个不通明的状态对象,其中一个或两个都能够为 null。Begin 办法负责初始化异步操作,如果提供了回调(通常称为初始操作的“连续”),它还负责确保在异步操作实现时调用回调。Begin 办法还将结构一个实现了 IAsyncResult 的类型实例,应用可选状态填充 IAsyncResult 的 AsyncState 属性:

namespace System
{
    public interface IAsyncResult
    {object? AsyncState { get;}
        WaitHandle AsyncWaitHandle {get;}
        bool IsCompleted {get;}
        bool CompletedSynchronously {get;}
    }
    public delegate void AsyncCallback(IAsyncResult ar);
}

而后,这个 IAsyncResult 实例将从 Begin 办法返回,并在最终调用 AsyncCallback 时传递给它。当筹备应用操作的后果时,调用者将把 IAsyncResult 实例传递给 End 办法,该办法负责确保操作已实现(如果没有实现,则通过阻塞同步期待操作实现),而后返回操作的任何后果,包含流传可能产生的任何谬误和异样。因而,不必像上面这样写代码来同步执行操作:

try
{int i = handler.DoStuff(arg);
    Use(i);
}
catch (Exception e)
{... // handle exceptions from DoStuff and Use}

能够按以下形式应用 Begin/End 办法异步执行雷同的操作:

try
{
    handler.BeginDoStuff(arg, iar =>
    {
        try
        {Handler handler = (Handler)iar.AsyncState!;
            int i = handler.EndDoStuff(iar);
            Use(i);
        }
        catch (Exception e2)
        {... // handle exceptions from EndDoStuff and Use}
    }, handler);
}
catch (Exception e)
{... // handle exceptions thrown from the synchronous call to BeginDoStuff}

对于在任何语言中解决过基于回调的 API 的人来说,这应该感觉很相熟。

然而,事件从此变得更加简单。例如,有一个 ”stack dives” 的问题。stack dives 是指代码重复调用,在堆栈中越陷越深,以至于可能呈现堆栈溢出。如果操作同步实现,Begin 办法被容许同步调用回调,这意味着对 Begin 的调用自身可能间接调用回调。同步实现的 “ 异步 “ 操作实际上是很常见的;它们不是 “ 异步 ”,因为它们被保障异步实现,而只是被容许这样做。

这是一种实在的可能性,很容易再现。在 .NET Core 上试试这个程序:

using System.NET;
using System.NET.Sockets;
using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
listener.Listen();
using Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
client.Connect(listener.LocalEndPoint!);
using Socket server = listener.Accept();
_ = server.SendAsync(new byte[100_000]);
var mres = new ManualResetEventSlim();
byte[] buffer = new byte[1];
var stream = new NetworkStream(client);
void ReadAgain()
{
    stream.BeginRead(buffer, 0, 1, iar =>
    {if (stream.EndRead(iar) != 0)
        {ReadAgain(); // uh oh!
        }
        else
        {mres.Set();
        }
    }, null);
};
ReadAgain();
mres.Wait();

在这里,我设置了一个相互连接的简略客户端套接字和服务器套接字。服务器向客户端发送 100,000 字节,而后客户端持续应用 BeginRead/EndRead 来“异步”地每次读取一个字节。传给 BeginRead 的回调函数通过调用 EndRead 来实现读取,而后如果它胜利读取了所需的字节,它会通过递归调用 ReadAgain 部分函数来收回另一个 BeginRead。然而,在 .NET Core 中,套接字操作比在 .NET Framework 上快得多,并且如果操作系统可能满足同步操作,它将同步实现(留神内核自身有一个缓冲区用于满足套接字接管操作)。因而,这个堆栈会溢出:

因而,APM 模型中内置了弥补机制。有两种可能的办法能够补救这一点:

1. 不要容许 AsyncCallback 被同步调用。如果始终异步调用它,即便操作以同步形式实现,那么 stack dives 的危险也会隐没。然而性能也是如此,因为同步实现的操作 (或者快到无奈察看到它们的区别) 是十分常见的,强制每个操作排队回调会减少可测量的开销。
2. 应用一种机制,容许调用方而不是回调方在操作同步实现时执行连续工作。这样,您就能够避开额定的办法框架,继续执行后续工作,而不深刻堆栈。

APM 模式与办法 2 一起应用。为此,IAsyncResult 接口公开了两个相干但不同的成员:IsCompleted 和 CompletedSynchronously。IsCompleted 通知你操作是否曾经实现,能够屡次查看它,最终它会从 false 转换为 true,而后放弃不变。相比之下,CompletedSynchronously 永远不会扭转(如果扭转了,那就是一个令人讨厌的 bug)。它用于 Begin 办法的调用者和 AsyncCallback 之间的通信,他们中的一个负责执行任何连续工作。如果 CompletedSynchronously 为 false,则操作是异步实现的,响应操作实现的任何后续工作都应该留给回调;毕竟,如果工作没有同步实现,Begin 的调用方无奈真正解决它,因为还不晓得操作曾经实现(如果调用方只是调用 End,它将阻塞直到操作实现)。然而,如果 CompletedSynchronously 为真,如果回调要解决连续工作,那么它就有 stack dives 的危险,因为它将在堆栈上执行比开始时更深的连续工作。因而,任何波及到这种堆栈潜水的实现都须要查看 CompletedSynchronously,并让 Begin 办法的调用者执行连续工作(如果它为真),这意味着回调不须要执行连续工作。这也是 CompletedSynchronously 永远不能更改的起因,调用方和回调方须要看到雷同的值,以确保不论竞争条件如何,连续工作只执行一次。

咱们都习惯了古代语言中的控制流构造为咱们提供的弱小和简略性,一旦引入了任何正当的复杂性,而基于回调的办法通常会与这种构造相冲突。其余支流语言也没有更好的代替计划。

咱们须要一种更好的办法,一种从 APM 模式中学习的办法,交融它正确的货色,同时防止它的陷阱。值得注意的是,APM 模式只是一种模式。运行工夫、外围库和编译器在应用或实现该模式时并没有提供任何帮忙。

基于事件的异步模式

.NET Framework 2.0 引入了一些 API,实现了解决异步操作的不同模式,这种模式次要用于在客户端应用程序上下文中解决异步操作。这种基于事件的异步模式或 EAP 也作为一对成员呈现,这次是一个用于初始化异步操作的办法和一个用于侦听其实现的事件。因而,咱们之前的 DoStuff 示例可能被公开为一组成员,如下所示:

class Handler
{public int DoStuff(string arg);
    public void DoStuffAsync(string arg, object? userToken);
    public event DoStuffEventHandler? DoStuffCompleted;
}
public delegate void DoStuffEventHandler(object sender, DoStuffEventArgs e);
public class DoStuffEventArgs : AsyncCompletedEventArgs
{public DoStuffEventArgs(int result, Exception? error, bool canceled, object? userToken) :
        base(error, canceled, usertoken) => Result = result;
    public int Result {get;}
}

你须要用 DoStuffCompleted 事件注册你的后续工作,而后调用 DoStuffAsync 办法; 它将启动该操作,并且在该操作实现时,调用者将异步地引发 DoStuffCompleted 事件。而后,处理程序能够继续执行后续工作,可能会验证所提供的 userToken 与它所冀望的进行匹配,从而容许多个处理程序同时连贯到事件。

这种模式使一些用例变得更简略,同时使其余用例变得更加艰难(思考到后面的 APM CopyStreamToStream 示例,这阐明了一些问题)。它没有以宽泛的形式推出,只是在一个独自的 .NET Framework 版本中匆匆的呈现又隐没了,只管留下了它应用期间增加的 api,如 Ping.SendAsync/Ping.PingCompleted:

public class Ping : Component
{public void SendAsync(string hostNameOrAddress, object? userToken);
    public event PingCompletedEventHandler? PingCompleted;
    ...
}

然而,它的确获得了一个 APM 模式齐全没有思考到的显著提高,并且这一点始终连续到咱们明天所承受的模型中: SynchronizationContext

思考到像 Windows Forms 这样的 UI 框架。与 Windows 上的大多数 UI 框架一样,控件与特定的线程相关联,该线程运行一个音讯泵,该音讯泵运行可能与这些控件交互的工作,只有该线程应该尝试操作这些控件,而任何其余想要与控件交互的线程都应该通过发送音讯由 UI 线程的泵耗费来实现操作。Windows 窗体应用 ControlBeginInvoke 等办法使这变得很容易,它将提供的委托和参数排队,由与该控件相关联的任何线程运行。因而,你能够这样编写代码:

private void button1_Click(object sender, EventArgs e)
{
    ThreadPool.QueueUserWorkItem(_ =>
    {string message = ComputeMessage();
        button1.BeginInvoke(() =>
        {button1.Text = message;});
    });
}

这将卸载在 ThreadPool 线程上实现的 ComputeMessage()工作(以便在解决 UI 的过程中放弃 UI 的响应性),而后在工作实现时,将委托队列返回到与 button1 相干的线程,以更新 button1 的标签。这很简略,WPF 也有相似的货色,只是用它的 Dispatcher 类型:

private void button1_Click(object sender, RoutedEventArgs e){
ThreadPool.QueueUserWorkItem(_ =>
{string message = ComputeMessage();
button1.Dispatcher.InvokeAsync(() =>
{button1.Content = message;});
});}

.NET MAUI 也有相似的性能。但如果我想把这个逻辑放到辅助办法中呢?

E.g.

// Call ComputeMessage and then invoke the update action to update controls.internal static void ComputeMessageAndInvokeUpdate(Action<string> update) {...}

而后我能够这样应用它:

private void button1_Click(object sender, EventArgs e){ComputeMessageAndInvokeUpdate(message => button1.Text = message);}

然而如何实现 ComputeMessageAndInvokeUpdate,使其可能在这些应用程序中工作呢? 是否须要硬编码能力理解每个可能的 UI 框架?这就是 SynchronizationContext 的魅力所在。咱们能够这样实现这个办法:

internal static void ComputeMessageAndInvokeUpdate(Action<string> update){
    SynchronizationContext? sc = SynchronizationContext.Current;
    ThreadPool.QueueUserWorkItem(_ =>
    {string message = ComputeMessage();
        if (sc is not null)
        {sc.Post(_ => update(message), null);
        }
        else
        {update(message);
        }
    });}

它应用 SynchronizationContext 作为一个形象,指标是任何“调度器”,应该用于回到与 UI 交互的必要环境。而后,每个应用程序模型确保它作为 SynchronizationContext.Current 公布一个 SynchronizationContext-derived 类型,去做 “ 正确的事件 ”。例如,Windows Forms 有这个:

public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable{public override void Post(SendOrPostCallback d, object? state) =>
        _controlToSendTo?.BeginInvoke(d, new object?[] {state});
    ...}

WPF 有这个:

public sealed class DispatcherSynchronizationContext : SynchronizationContext{public override void Post(SendOrPostCallback d, Object state) =>
        _dispatcher.BeginInvoke(_priority, d, state);
    ...}

ASP.NET 已经有一个,它实际上并不关怀工作在什么线程上运行,而是关怀给定的申请相干的工作被序列化,这样多个线程就不会并发地拜访给定的 HttpContext:

internal sealed class AspNetSynchronizationContext : AspNetSynchronizationContextBase{public override void Post(SendOrPostCallback callback, Object state) =>
        _state.Helper.QueueAsynchronous(() => callback(state));
    ...}

这也不限于这些次要的应用程序模型。例如,xunit 是一个风行的单元测试框架,是 .NET 外围存储库用于单元测试的框架,它也采纳了多个自定义的 SynchronizationContext。例如,你能够容许并行运行测试,但限度容许并发运行的测试数量。这是如何实现的呢?通过 SynchronizationContext:

public class MaxConcurrencySyncContext : SynchronizationContext, IDisposable{public override void Post(SendOrPostCallback d, object? state)
    {var context = ExecutionContext.Capture();
        workQueue.Enqueue((d, state, context));
        workReady.Set();}}

MaxConcurrencySyncContext 的 Post 办法只是将工作排到本人的外部工作队列中,而后在它本人的工作线程上解决它,它依据所需的最大并发数来管制有多少工作线程。

这与基于事件的异步模式有什么分割?EAP 和 SynchronizationContext 是同时引入的,当异步操作被启动时,EAP 规定实现事件应该排队到以后任何 SynchronizationContext 中。为了略微简化一下,System.ComponentModel 中也引入了一些辅助类型,尤其是 AsyncOperation 和 AsyncOperationManager。前者只是一个元组,封装了用户提供的状态对象和捕捉的 SynchronizationContext,而后者只是作为一个简略的工厂来捕捉并创立 AsyncOperation 实例。而后 EAP 实现将应用这些,例如 Ping.SendAsync 调用 AsyncOperationManager.CreateOperation 来捕捉 SynchronizationContext。当操作实现时,AsyncOperation 的 PostOperationCompleted 办法将被调用,以调用存储的 SynchronizationContext 的 Post 办法。

咱们须要比 APM 模式更好的货色,接下来呈现的 EAP 引入了一些新的事务,但并没有真正解决咱们面临的外围问题。咱们依然须要更好的货色。

输出工作

.NET Framework 4.0 引入了 System.Threading.Tasks.Task 类型。从实质上讲,Task 只是一个数据结构,示意某些异步操作的最终实现(其余框架将相似的类型称为“promise”或“future”)。创立 Task 是为了示意某些操作,而后当它示意的操作逻辑上实现时,后果存储到该 Task 中。然而 Task 提供的要害个性使它比 IAsyncResult 更有用,它在本人外部内置了 continuation 的概念。这一个性意味着您能够拜访任何 Task,并在其实现时申请异步告诉,由工作自身解决同步,以确保持续被调用,无论工作是否曾经实现、尚未实现、还是与告诉申请同时实现。为什么会有如此大的影响?如果你还记得咱们对旧 APM 模式的探讨,有两个次要问题。

  1. 你必须为每个操作实现一个自定义的 IAsyncResult 实现:没有内置的 IAsyncResult 实现,任何人都能够依据须要应用。
  2. 在 Begin 办法被调用之前,你必须晓得当它实现时要做什么。这使得实现组合器和其余用于耗费和组合任意异步实现的通用例程成为一个重大挑战。

当初,让咱们更好地了解它的理论含意。咱们先从几个字段开始:

class MyTask{
    private bool _completed;
    private Exception? _error;
    private Action<MyTask>? _continuation;
    private ExecutionContext? _ec;
    ...}

咱们须要一个字段来晓得工作是否实现(_completed),还须要一个字段来存储导致工作失败的任何谬误(_error);如果咱们还要实现一个通用的 MyTask<TResult>,那么也会有一个公有的 TResult _result 字段,用于存储操作的胜利后果。到目前为止,这看起来很像咱们之前自定义的 IAsyncResult 实现(当然,这不是偶合)。然而当初最重要的局部,是 _continuation 字段。在这个简略的实现中,咱们只反对一个 continuation,但对于解释目标来说这曾经足够了(真正的工作应用了一个对象字段,该字段能够是单个 continuation 对象,也能够是 continuation 对象的 List<>)。这是一个委托,将在工作实现时调用。

如前所述,与以前的模型相比,Task 的一个根本提高是可能在操作开始后提供连续工作(回调)。咱们须要一个办法来做到这一点,所以让咱们增加 ContinueWith:

public void ContinueWith(Action<MyTask> action){lock (this)
    {if (_completed)
        {ThreadPool.QueueUserWorkItem(_ => action(this));
        }
        else if (_continuation is not null)
        {throw new InvalidOperationException("Unlike Task, this implementation only supports a single continuation.");
        }
        else
        {
            _continuation = action;
            _ec = ExecutionContext.Capture();}
    }}

如果工作在 ContinueWith 被调用时曾经被标记为实现,ContinueWith 只是排队执行委托。否则,该办法将存储该委托,以便在工作实现时能够排队继续执行(它还存储了一个叫做 ExecutionContext 的货色,而后在当前调用该委托时应用它)。

而后,咱们须要可能将 MyTask 标记为实现,这意味着它所代表的异步操作曾经实现。为此,咱们将提供两个办法,一个用于标记实现(” SetResult “),另一个用于标记实现并返回谬误(” SetException “):

public void SetResult() => Complete(null);
public void SetException(Exception error) => Complete(error);
private void Complete(Exception? error){lock (this)
    {if (_completed)
        {throw new InvalidOperationException("Already completed");
        }

        _error = error;
        _completed = true;

        if (_continuation is not null)
        {
            ThreadPool.QueueUserWorkItem(_ =>
            {if (_ec is not null)
                {ExecutionContext.Run(_ec, _ => _continuation(this), null);
                }
                else
                {_continuation(this);
                }
            });
        }
    }}

咱们存储任何谬误,将工作标记为已实现,而后如果之前曾经注册了 continuation,则将其排队期待调用。

最初,咱们须要一种办法来流传工作中可能产生的任何异样(并且,如果这是一个泛型 MyTask<T>,则返回其_result);为了不便某些状况,咱们还容许此办法阻塞期待工作实现,这能够通过 ContinueWith 实现(continuation 只是收回 ManualResetEventSlim 信号,而后调用者阻塞期待实现)。

public void Wait(){
    ManualResetEventSlim? mres = null;
    lock (this)
    {if (!_completed)
        {mres = new ManualResetEventSlim();
            ContinueWith(_ => mres.Set());
        }
    }

    mres?.Wait();
    if (_error is not null)
    {ExceptionDispatchInfo.Throw(_error);
    }}

基本上就是这样。当初能够必定的是,真正的 Task 要简单得多,有更高效的实现,反对任意数量的 continuation,有大量对于它应该如何体现的按钮(例如,continuation 应该像这里所做的那样排队,还是应该作为工作实现的一部分同步调用),可能存储多个异样而不是一个异样,具备勾销的非凡常识,有大量的辅助办法用于执行常见操作,例如 Task.Run,它创立一个 Task 来示意线程池上调用的委托队列等等。

你可能还留神到,我简略的 MyTask 间接有公共的 SetResult/SetException 办法,而 Task 没有。实际上,Task 的确有这样的办法,它们只是外部的,System.Threading.Tasks.TaskCompletionSource 类型作为工作及其实现的独立“生产者”; 这样做不是出于技术上的须要,而是为了让实现办法远离只用于生产的货色。而后,你就能够把 Task 散发进来,而不用放心它会在你上面实现; 实现信号是创立工作的实现细节,并且通过保留 TaskCompletionSource 自身来保留实现它的权力。(CancellationToken 和 CancellationTokenSource 遵循相似的模式:CancellationToken 只是 CancellationTokenSource 的一个构造封装器,只提供与生产勾销信号相干的公共区域,但没有产生勾销信号的能力,而产生勾销信号的能力仅限于可能拜访 CancellationTokenSource 的人。)

当然,咱们能够为这个 MyTask 实现组合器和辅助器,就像 Task 提供的那样。想要一个简略的 MyTask.WhenAll?

public static MyTask WhenAll(MyTask t1, MyTask t2){var t = new MyTask();

    int remaining = 2;
    Exception? e = null;

    Action<MyTask> continuation = completed =>
    {
        e ??= completed._error; // just store a single exception for simplicity
        if (Interlocked.Decrement(ref remaining) == 0)
        {if (e is not null) t.SetException(e);
            else t.SetResult();}
    };

    t1.ContinueWith(continuation);
    t2.ContinueWith(continuation);

    return t;}

想要一个 MyTask.Run?你失去了它:

public static MyTask Run(Action action){var t = new MyTask();

    ThreadPool.QueueUserWorkItem(_ =>
    {
        try
        {action();
            t.SetResult();}
        catch (Exception e)
        {t.SetException(e);
        }
    });

    return t;}

一个 MyTask.Delay 怎么样?当然能够:

public static MyTask Delay(TimeSpan delay){var t = new MyTask();

    var timer = new Timer(_ => t.SetResult());
    timer.Change(delay, Timeout.InfiniteTimeSpan);

    return t;}

有了 Task,.NET 中之前的所有异步模式都将成为过来。在以前应用 APM 模式或 EAP 模式实现异步实现的中央,都会公开新的 Task 返回办法。

▌ValueTasks

时至今日,Task 依然是 .NET 中异步解决的主力,每次公布都有新办法公开,并且在整个生态系统中都例行地返回 Task 和  Task<TResult>。然而,Task 是一个类,这意味着创立一个类须要分配内存。在大多数状况下,为一个长期异步操作额定分配内存是微不足道的,除了对性能最敏感的操作之外,不会对所有操作的性能产生有意义的影响。不过,如前所述,异步操作的同步实现是相当常见的。引入 Stream.ReadAsync 是为了返回一个 Task<int>,但如果你从一个 BufferedStream 中读取数据,很有可能很多读取都是同步实现的,因为只须要从内存中的缓冲区中读取数据,而不是执行零碎调用和真正的 I/O 操作。不得不调配一个额定的对象来返回这样的数据是可怜的(留神,APM 也是这样的状况)。对于返回 Task 的非泛型办法,该办法能够只返回一个曾经实现的单例工作,而实际上 Task.CompletedTask 提供了一个这样的单例 Task。但对于 Task<TResult> 来说,不可能为每个可能的后果缓存一个 Task。咱们能够做些什么来让这种同步实现更快呢?

缓存一些 Task<TResult> 是可能的。例如,Task<bool> 十分常见,而且只有两个有意义的货色须要缓存:当后果为 true 时,一个 Task<bool>,当后果为 false 时,一个 Task<bool>。或者,尽管咱们不想缓存 40 亿个 Task<int> 来包容所有可能的 Int32 后果,但小的 Int32 值是十分常见的,因而咱们能够缓存一些值,比方 - 1 到 8。或者对于任意类型,default 是一个正当的通用值,因而咱们能够缓存每个相干类型的 Task<TResult>,其中 Result 为 default(TResult)。事实上,Task.FromResult 明天也是这样做的(从最近的 .NET 版本开始),应用一个小型的可复用的 Task<TResult> 单例缓存,并在适当时返回其中一个,或者为精确提供的后果值调配一个新的 Task<TResult>。能够创立其余计划来解决其余正当的常见状况。例如,当应用 Stream.ReadAsync 时,在同一个流上屡次调用它是正当的,而且每次调用时容许读取的字节数都是雷同的。实现可能齐全满足 count 申请是正当的。这意味着 Stream.ReadAsync 反复返回雷同的 int 值是很常见的。为了防止这种状况下的屡次调配,多个 Stream 类型(如 MemoryStream)会缓存它们最初胜利返回的 Task<int>,如果下一次读取也同步实现并胜利取得雷同的后果,它能够只是再次返回雷同的 Task<int>,而不是创立一个新的。但其余状况呢?在性能开销十分重要的状况下,如何更广泛地防止对同步实现的这种调配?

这就是 ValueTask<TResult> 的作用。ValueTask<TResult> 最后是作为 TResult 和 Task<TResult> 之间的一个辨别并集。说到底,抛开那些花哨的货色,这就是它的全副 (或者,更确切地说,已经是),是一个即时的后果,或者是对将来某个时刻的一个后果的承诺:

public readonly struct ValueTask<TResult>{
   private readonly Task<TResult>? _task;
   private readonly TResult _result;
   ...}

而后,一个办法能够返回这样一个 ValueTask<TResult>,而不是一个 Task<TResult>,如果 TResult 在须要返回的时候曾经晓得了,那么就能够防止 Task<TResult> 的调配,代价是一个更大的返回类型和略微多一点间接性。

然而,在一些超级极其的高性能场景中,即便在异步实现的状况下,您也心愿可能防止 Task<TResult> 调配。例如,Socket 位于网络堆栈的底部,Socket 上的 SendAsync 和 ReceiveAsync 对于许多服务来说是十分热门的门路,同步和异步实现都十分常见(大多数同步发送实现,许多同步接管实现,因为数据曾经在内核中缓冲了)。如果在一个给定的 Socket 上,咱们能够使这样的发送和接管不受调配限度,而不论操作是同步实现还是异步实现,这不是很好吗?

这就是 System.Threading.Tasks.Sources.IValueTaskSource<TResult> 进入的中央:

public interface IValueTaskSource<out TResult>{ValueTaskSourceStatus GetStatus(short token);
    void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags);
    TResult GetResult(short token);}

IValueTaskSource<TResult> 接口容许一个实现为 ValueTask<TResult> 提供本人的反对对象,使该对象可能实现像 GetResult 这样的办法来检索操作的后果,以及 OnCompleted 来连贯操作的连续。就这样,ValueTask<TResult> 对其定义进行了一个小小的更改,其 Task<TResult>? _task 字段替换为 object? _obj 字段:

public readonly struct ValueTask<TResult>{
   private readonly object? _obj;
   private readonly TResult _result;
   ...}

以前 _task 字段要么是 Task<TResult> 要么是 null,当初 _obj 字段也能够是 IValueTaskSource<TResult>。一旦 Task<TResult> 被标记为已实现,它将放弃实现状态,并且永远不会转换回未实现的状态。相比之下,实现 IValueTaskSource<TResult> 的对象对实现有齐全的控制权,能够自在地在实现状态和不实现状态之间双向转换,因为 ValueTask<TResult> 的契约是一个给定的实例只能被耗费一次,因而从构造上看,它不应该察看到底层实例的耗费后变动(这就是 CA2012 等剖析规定存在的起因)。这就使得像 Socket 这样的类型可能将 IValueTaskSource<TResult> 的实例集中起来,用于反复调用。Socket 最多能够缓存两个这样的实例,一个用于读,一个用于写,因为 99.999% 的状况是在同一时间最多只有一个接管和一个发送。

我提到了 ValueTask<TResult>,但没有提到 ValueTask。当只解决防止同步实现的调配时,应用非泛型 ValueTask(代表无后果的有效操作)在性能上没有什么益处,因为同样的条件能够用 Task.CompletedTask 来示意。然而,一旦咱们关怀在异步实现的状况下应用可池化的底层对象来防止调配的能力,那么这对非泛型也很重要。因而,当 IValueTaskSource<TResult> 被引入时,IValueTaskSource 和 ValueTask 也被引入。

因而,咱们有 Task、Task<TResult>、ValueTask 和 ValueTask<TResult>。咱们可能以各种形式与它们交互,示意任意的异步操作,并连贯 continuation 来解决这些异步操作的实现。

下期文章,咱们将持续介绍 C# 迭代器,欢送继续关注。

正文完
 0