首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Webpack5入门与实战,前端开发必备技能-碧山留岂得,芳草怨相违

async/await 在 C# 言语中是如何工作的?

Webpack5入门与实战,前端开发必备技能

download:https://www.zxit666.com/5600/

它提供了对平台的高层次概述,总结了各种组件和设计决策,并承诺对所触及的范畴发表更深化的文章。这是第一篇这样深化讨论 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 update) { ... }

然后我能够这样运用它:

private void button1_Click(object sender, EventArgs e){

ComputeMessageAndInvokeUpdate(message => button1.Text = message);}

但是如何完成 ComputeMessageAndInvokeUpdate,使其可以在这些应用程序中工作呢?能否需求硬编码才干理解每个可能的 UI 框架?这就是 SynchronizationContext 的魅力所在。我们能够这样完成这个办法:

internal static void ComputeMessageAndInvokeUpdate(Action 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 形式的讨论,有两个主要问题。

你必需为每个操作完成一个自定义的 IAsyncResult 完成:没有内置的 IAsyncResult 完成,任何人都能够依据需求运用。

在 Begin 办法被调用之前,你必需晓得当它完成时要做什么。这使得完成组合器和其他用于耗费和组合恣意异步完成的通用例程成为一个严重应战。

如今,让我们更好天文解它的实践含义。我们先从几个字段开端:

class MyTask{

private bool _completed;

private Exception? _error;

private Action? _continuation;

private ExecutionContext? _ec;

...}

我们需求一个字段来晓得任务能否完成(_completed),还需求一个字段来存储招致任务失败的任何错误(_error);假如我们还要完成一个通用的 MyTask,那么也会有一个私有的 TResult _result 字段,用于存储操作的胜利结果。到目前为止,这看起来很像我们之前自定义的 IAsyncResult 完成(当然,这不是巧合)。但是如今最重要的局部,是 _continuation 字段。在这个简单的完成中,我们只支持一个 continuation,但关于解释目的来说这曾经足够了(真正的任务运用了一个对象字段,该字段能够是单个 continuation 对象,也能够是 continuation 对象的 List)。这是一个拜托,将在任务完成时调用。

如前所述,与以前的模型相比,Task 的一个根本进步是可以在操作开端后提供持续工作(回调)。我们需求一个办法来做到这一点,所以让我们添加 ContinueWith:

public void ContinueWith(Action 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,则返回其_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 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。但是,Task 是一个类,这意味着创立一个类需求分配内存。在大多数状况下,为一个长期异步操作额外分配内存是微乎其微的,除了对性能最敏感的操作之外,不会对一切操作的性能产生有意义的影响。不过,如前所述,异步操作的同步完成是相当常见的。引入 Stream.ReadAsync 是为了返回一个 Task,但假如你从一个 BufferedStream 中读取数据,很有可能很多读取都是同步完成的,由于只需求从内存中的缓冲区中读取数据,而不是执行系统调用和真正的 I/O 操作。不得不分配一个额外的对象来返回这样的数据是不幸的(留意,APM 也是这样的状况)。关于返回 Task 的非泛型办法,该办法能够只返回一个曾经完成的单例任务,而实践上 Task.CompletedTask 提供了一个这样的单例 Task。但关于 Task 来说,不可能为每个可能的结果缓存一个 Task。我们能够做些什么来让这种同步完成更快呢?

缓存一些 Task 是可能的。例如,Task 十分常见,而且只要两个有意义的东西需求缓存:当结果为 true 时,一个 Task,当结果为 false 时,一个 Task。或者,固然我们不想缓存40亿个 Task 来包容一切可能的 Int32 结果,但小的 Int32 值是十分常见的,因而我们能够缓存一些值,比方-1到8。或者关于恣意类型,default 是一个合理的通用值,因而我们能够缓存每个相关类型的 Task,其中 Result 为 default(TResult)。事实上,Task.FromResult 今天也是这样做的 (从最近的 .NET 版本开端),运用一个小型的可复用的 Task 单例缓存,并在恰当时返回其中一个,或者为精确提供的结果值分配一个新的 Task。能够创立其他计划来处置其他合理的常见状况。例如,当运用 Stream.ReadAsync 时,在同一个流上屡次调用它是合理的,而且每次调用时允许读取的字节数都是相同的。完成可以完整满足 count 恳求是合理的。这意味着 Stream.ReadAsync 反复返回相同的 int 值是很常见的。为了防止这种状况下的屡次分配,多个 Stream 类型(如 MemoryStream)会缓存它们最后胜利返回的 Task,假如下一次读取也同步完成并胜利取得相同的结果,它能够只是再次返回相同的 Task,而不是创立一个新的。但其他状况呢?在性能开支十分重要的状况下,如何更普遍地防止对同步完成的这种分配?

这就是 ValueTask 的作用。ValueTask 最初是作为 TResult 和 Task 之间的一个辨别并集。说到底,抛开那些花哨的东西,这就是它的全部 (或者,更确切地说,曾经是),是一个即时的结果,或者是对将来某个时辰的一个结果的承诺:

public readonly struct ValueTask{

private readonly Task? _task;

private readonly TResult _result;

...}

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

但是,在一些超级极端的高性能场景中,即便在异步完成的状况下,您也希望可以防止 Task 分配。例如,Socket 位于网络堆栈的底部,Socket 上的 SendAsync 和 ReceiveAsync 关于许多效劳来说是十分抢手的途径,同步和异步完成都十分常见(大多数同步发送完成,许多同步接纳完成,由于数据曾经在内核中缓冲了)。假如在一个给定的 Socket 上,我们能够使这样的发送和接纳不受分配限制,而不论操作是同步完成还是异步完成,这不是很好吗?

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

public interface IValueTaskSource{

ValueTaskSourceStatus GetStatus(short token);

void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags);

TResult GetResult(short token);}

IValueTaskSource 接口允许一个完成为 ValueTask 提供本人的支持对象,使该对象可以完成像 GetResult 这样的办法来检索操作的结果,以及 OnCompleted 来衔接操作的持续。就这样,ValueTask 对其定义停止了一个小小的更改,其 Task? _task 字段交换为 object? _obj 字段:

public readonly struct ValueTask{

private readonly object? _obj;

private readonly TResult _result;

...}

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

我提到了 ValueTask,但没有提到 ValueTask。当只处置防止同步完成的分配时,运用非泛型 ValueTask(代表无结果的无效操作)在性能上没有什么益处,由于同样的条件能够用 Task.CompletedTask 来表示。但是,一旦我们关怀在异步完成的状况下运用可池化的底层对象来防止分配的才能,那么这对非泛型也很重要。因而,当 IValueTaskSource 被引入时,IValueTaskSource 和 ValueTask 也被引入。

因而,我们有 Task、Task、ValueTask 和 ValueTask。我们可以以各种方式与它们交互,表示恣意的异步操作,并衔接 continuation 来处置这些异步操作的完成。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20230406A00AM300?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券