如何在任何情况下使用C#等待关键字

介绍 (Introduction)

The async/await keywords are a powerful way to add asynchronicity to your applications, improving performance and responsiveness. If you know how to do it, you can make your own awaitable members and types and take advantage of awaiting on anything you like. Here, we peek behind the scenes at await and walk through a couple of different methods for making something awaitable, plus how to choose which one is appropriate.

async / await关键字是一种增加应用程序异步性,改善性能和响应能力的强大方法。 如果您知道该怎么做,则可以创建自己的等待成员和类型,并利用等待中的任何机会。 在这里,我们在await在后台进行窥视,并通过几种不同的方法使await东西,以及如何选择合适的方法。

概念化这个混乱 (Conceptualizing this Mess)

While I was creating an awaitable socket library I learned a little bit about how to extend awaitable features to things other than Tasks. Thanks to some kind folks here, I happened upon this article by Stephen Toub, who I follow anyway. It explains what I'm about to, but he's very brief about it, and he writes his articles for an advanced audience which makes for a challenging read. We're going to revisit some of his code, and I'll endeavor to make it more accessible to a wider audience of developers.

在创建等待套接字库时,我学到了一些有关如何将等待功能扩展到Task以外的东西的知识。 感谢这里的好心人,我偶然看到了斯蒂芬·图布(Stephen Toub)的这篇文章 。 它解释了我要做什么,但是他对此非常简短,并且他为高级读者撰写了文章,这给阅读带来了挑战。 我们将重新审视他的一些代码,并且我将努力使其对更广泛的开发人员开放。

待定类型 (Awaitable Types)

An awaitable type is one that includes at least a single instance method called GetAwaiter() which retrieves an instance of an awaiter type. These can also be implemented as extension methods. Theoretically, you could make an extension method for say, int and make it return an awaiter that represents the current asynchronous operation, such as delaying by the specified integer amount. Using it would be like await 1500 to delay for 1500 milliseconds. We'll be doing exactly that later. The point is that anything that implements GetAwaiter() (either directly or via an extension method) and returns an awaiter object can be awaited on. Task exposes this, and it's the reason a task can be awaited on.

一种等待类型是至少包含一个称为GetAwaiter()实例方法,该方法检索一个等待者类型的实例。 这些也可以实现为扩展方法。 从理论上讲,您可以将一个扩展方法声明为int并使其返回表示当前异步操作的等待者,例如延迟指定的整数量。 使用它就像await 1500延迟1500毫秒。 稍后我们会做的。 关键是可以GetAwaiter()实现GetAwaiter() (直接或通过扩展方法)并返回awaiter对象的任何对象。 Task揭示了这一点,这就是可以等待任务的原因。

服务员类型 (Awaiter Types)

The type that GetAwaiter() returns must implement System.Runtime.CompilerServices.INotifyCompletion or the corresponding ICriticalNotifyCompletion interfaces. In addition to implementing the interface's OnCompleted() method, it must also implement two members, called IsCompleted and GetResult() that aren't part of any interface.

GetAwaiter()返回的类型必须实现System.Runtime.CompilerServices.INotifyCompletion或相应的ICriticalNotifyCompletion接口。 除了实现该接口的OnCompleted()方法之外,它还必须实现两个成员IsCompletedGetResult() ,它们不是任何接口的一部分。

TaskAwaiter (The TaskAwaiter)

TaskAwaiter exposes all of the awaiter object members, and can be returned from Task. Sometimes, we'll be starting a new task and returning its awaiter in order to simplify things. However, since it's only returned by Task, we can't use it to return things not associated with a task. If you want to make something awaitable that does not use Task to perform its work, you must create your own awaiter object.

TaskAwaiter公开所有等待对象对象,并且可以从Task返回。 有时,我们将开始一个新任务并返回其等待者,以简化操作。 但是,由于它仅由Task返回,因此我们不能使用它来返回与任务无关的内容。 如果要使某些不使用Task来执行其工作的等待对象,则必须创建自己的waiter对象。

Let's get to the code!

让我们看一下代码!

编码此混乱 (Coding this Mess)

简单案例:使用TaskAwaiter (The Simple Case: Using TaskAwaiter)

On an static class, we can implement the following extension method:

在静态类上,我们可以实现以下扩展方法:

internal static TaskAwaiter GetAwaiter(this int milliseconds)
    => Task.Delay(milliseconds).GetAwaiter();

Now you can perform an await on an int and it will wait for the specified number of milliseconds. Remember, anything that starts a Task (like Task.Delay() does) can be used this way. Like I said though, if your operation does not spawn a task whose awaiter you can return, you must implement your own awaiter. Let's look at another example similar to that of the above - this one from Stephen Toub:

现在,您可以在一个int上执行await ,它将等待指定的毫秒数。 请记住,任何启动Task东西(例如Task.Delay()都可以)以这种方式使用。 就像我说的那样,如果您的操作没有产生可以返回其等待者的任务,则必须实现自己的等待者。 让我们看一下与上面类似的另一个示例-Stephen Toub的这个示例:

public static TaskAwaiter GetAwaiter(this TimeSpan timeSpan)
{
    return Task.Delay(timeSpan).GetAwaiter();
}

You can see this does the same thing, except for with TimeSpan instead of int, meaning you can await a TimeSpan instance too. You don't have to use extension methods if you can put the GetAwaiter() method directly on your type, in which case it shouldn't be static. Doing this will make your type awaitable just like the extension methods do for other types.

您可以看到它执行相同的操作,除了使用TimeSpan而不是int ,这意味着您也可以await TimeSpan实例。 如果可以将GetAwaiter()方法直接放在您的类型上,则不必使用扩展方法,在这种情况下,它不应是static 。 这样做将使您的类型可以等待,就像扩展方法对其他类型所做的那样。

Now we can do:

现在我们可以做:

await 1500; // wait for 1500ms

and:

和:

await new TimeSpan(0, 0, 0, 2); // wait for 2 seconds

I don't actually recommend awaiters on most simple types because it's vague. What I mean is await 1500 says nothing about what it does, and that makes it harder to read. I feel the same with awaiting on a TimeSpan. This code is here to illustrate the concept. With the next bit of code, we'll produce something a little more realistic.

实际上,我不建议在大多数简单类型上使用等待者,因为它含糊不清。 我的意思是, await 1500没有说明它的功能,这使它很难阅读。 我对等待TimeSpan感到相同。 此处的代码说明了这一概念。 在接下来的代码中,我们将产生一些更实际的东西。

并非如此简单的案例:创建自定义等待者类型 (The Not So Simple Case: Creating a Custom Awaiter Type)

Sometimes, it doesn't make sense to spawn a Task to fulfill an operation. This can be the case if you're wrapping an asynchronous programming pattern that doesn't use Task. It can also be the case if your operation itself is simple. If you use all struct types for your awaitable type and/or your awaiter type, they will avoid heap allocation. As far as I'm told, running a Task requires at least one object to be allocated on the managed heap. Furthermore, a Task is simply complicated because it needs to be all things to all people. What we really want is a slim way to await.

有时,产生一个Task来完成一个操作是没有意义的。 如果您要包装不使用Task的异步编程模式,则可能是这种情况。 如果您的操作本身很简单,也可能是这种情况。 如果将所有struct类型都用于等待类型和/或等待类型,则它们将避免堆分配。 据我所知,运行Task需要在托管堆上分配至少一个对象。 此外, Task非常复杂,因为它需要所有人的所有东西。 我们真正想要的是一种苗条的await

In this case, we need to create an object implementing one of two interfaces: INotifyCompletion or INotifyCriticalCompletion. The latter does not copy the execution context, which means its potentially faster, but very dangerous as it can elevate code's privilege. Normally, you'll want to use the former, as the risks to code access security usually outweigh any performance gain. The single method, OnCompleted() gets called when the operation completes. This is where you would do any continuation. We'll get to that. Note that OnCompleted() should be public to avoid the framework boxing your struct, which it must do to access the interface. Boxing causes a heap allocation. If the method is public however, it can skip the boxing and access the method directly, I believe. I haven't dived into the IL to verify it yet, but it's not unlikely so this way we can handle that scenario efficiently.

在这种情况下,我们需要创建一个实现以下两个接口之一的对象: INotifyCompletionINotifyCriticalCompletion 。 后者不会复制执行上下文,这意味着它可能更快,但非常危险,因为它可以提高代码的特权。 通常,您会希望使用前者,因为代码访问安全性的风险通常会超过任何性能提升。 操作完成后,将调用单一方法OnCompleted() 。 在这里您可以进行任何继续操作。 我们将解决这个问题。 请注意, OnCompleted()应该是public以避免框架将struct装箱,以访问接口。 装箱会导致堆分配。 我相信,如果该方法是公开的,它可以跳过装箱并直接访问该方法。 我尚未深入IL进行验证,但这并不是不可能,因此我们可以通过这种方式有效地处理这种情况。

We must also implement IsCompleted and GetResult() which aren't part of any actual interface. The compiler generates code to call these methods, so it's not a runtime thing where interfaces or abstract classes would be the only way. The compiler doesn't need to access things through interfaces because there's no binary contract involved. It's simply that the compiler is generating the code to call the method at the source level, not having to resolve the call by calling through the interface's vtable (the list of function pointers that point to methods for an object in .NET) at runtime. I hope that's clear, but if it's a little confusing don't worry, as it's not important to understand this detail fully in order to use this technique.

我们还必须实现IsCompletedGetResult() ,它们不是任何实际接口的一部分。 编译器生成用于调用这些方法的代码,因此这不是运行时的接口或抽象类是唯一的方法。 编译器不需要通过接口访问内容,因为不涉及二进制协定。 很简单,编译器正在生成代码以在源级别调用该方法,而不必在运行时通过接口的vtable(指向.NET中对象方法的函数指针列表)进行调用来解决该调用。 我希望这很清楚,但是如果有点令人困惑,请不要担心,因为使用此技术完全了解这个细节并不重要。

In case it's not totally obvious, the IsCompleted property indicates whether or not the operation has been completed.

如果不是很明显,则IsCompleted属性指示该操作是否已完成。

The GetResult() method takes no arguments and the return type is the same return type of your pseudo-task's result. It can be void if it has no result. If this were the equivalent of a Task<int> you'd return int here. I hope that makes sense. This is where you want to do the primary work for the task. This method blocks, meaning your code can be synchronous. If retrieving the result failed (meaning the operation failed), this method is where you'd throw.

GetResult()方法不带任何参数,返回类型与伪任务结果的返回类型相同。 如果没有结果,则可以为void 。 如果这等效于Task<int> ,则将在此处返回int 。 我希望这是有道理的。 这是您要完成任务的主要工作的地方。 此方法将阻止,这意味着您的代码可以是同步的。 如果检索结果失败(这意味着操作失败),这种方法是你想throw

I was trying to think of a good use case for creating your own awaiter that wasn't too complex, and was having a difficult time of it. Fortunately, Sergey Tepliakov produced a fine example here in which I only had to modify OnCompleted() and IsCompleted a little bit. We'll explore it below:

我试图考虑一个很好的用例,以创建您自己的侍应生,它不太复杂,并且经历了一段艰难的时期。 幸运的是,谢尔盖·特普利亚科夫(Sergey Tepliakov) 在这里给出了一个很好的例子,其中我只需要修改OnCompleted()IsCompleted 。 我们将在下面进行探讨:

// modified from 
// https://devblogs.microsoft.com/premier-developer/extending-the-async-methods-in-c/
// premodifed source by Sergey Tepliakov
static class LazyUtility
{
    // our awaiter type
    public struct Awaiter<T> : INotifyCompletion
    {
        private readonly Lazy<T> _lazy;
            
        public Awaiter(Lazy<T> lazy) => _lazy = lazy;

        public T GetResult() => _lazy.Value;

        public bool IsCompleted => _lazy.IsValueCreated;

        public void OnCompleted(Action continuation)
        {
            // run the continuation if specified
            if (null != continuation)
                Task.Run(continuation);
        }
    }
    // extension method for Lazy<T>
    // required for await support
    public static Awaiter<T> GetAwaiter<T>(this Lazy<T> lazy)
    {
        return new Awaiter<T>(lazy);
    }
}

This extends Lazy<T> to be awaitable. All the work is done by Lazy<T> when we call its Value property in GetResult(). This way, if you have a long running initialization, you can complete it asynchronously simply by awaiting your Lazy<T> instance.

这将Lazy<T>扩展为可等待的。 当我们在GetResult()调用其Value属性时,所有工作均由Lazy<T> GetResult() 。 这样,如果长时间运行的初始化,只需等待Lazy<T>实例即可异步完成它。

Note we're never spawning a Task here. Again, GetResult() can block, like it would here, if your Lazy<T>'s initialization code is long running.

请注意,我们永远不会在此处生成Task 。 同样,如果Lazy<T>的初始化代码长时间运行,则GetResult()可以像在这里一样阻塞。

Also note that we take a Lazy<T> argument in the constructor, which is important for obvious reasons.

还要注意,我们在构造函数中采用了Lazy<T>参数,由于显而易见的原因,该参数很重要。

We're running continuation in OnCompleted() which allows for chaining tasks together with Task.ContinueWith() for example.

我们在OnCompleted()运行continuation ,它允许将任务与Task.ContinueWith()链接在一起。

You can see that we're also forwarding to IsValueCreated in IsCompleted. This lets the framework know if the work in GetResult() has finished which it has once Lazy<T> creates the value. Note that we're never creating a Task ourselves except when we run the continuation in OnCompleted(). This makes for a more efficient way to do awaiting than creating a Task for it.

您可以看到我们还将转发到IsCompleted IsValueCreated 。 这使框架知道一旦Lazy<T>创建值, GetResult()的工作是否已完成。 请注意,除非在OnCompleted()运行continuation ,否则我们永远不会自己创建Task 。 这比为它创建Task提供了一种更高效的等待方式。

Now we can do:

现在我们可以做:

var result = await myLazyT; // awaitable initialization of myLazyT

It should be noted that whatever you do in this class should be thread safe. Lazy<T> already is.

应该注意的是,您在此类中所做的任何操作都应该是线程安全的。 Lazy<T>已经是。

Hopefully, that should get you started creating awaitable objects. Enjoy!

希望这应该使您开始创建等待的对象。 请享用!

翻译自: https://www.codeproject.com/Articles/5274659/How-to-Use-the-Csharp-Await-Keyword-on-Anything

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值