【问题标题】:How to make Task awaitable如何使任务可等待
【发布时间】:2020-06-23 13:03:19
【问题描述】:

昨天我开始使用 Microsoft CTP 异步库,但在任何地方我都找不到等待任务的正确实现。我知道它必须有这样的实现?:

public struct SampleAwaiter<T>
{
    private readonly Task<T> task;
    public SampleAwaiter(Task<T> task) { this.task = task; }
    public bool IsCompleted { get { return task.IsCompleted; } }
    public void OnCompleted(Action continuation) { TaskEx.Run(continuation); }
    public T GetResult() { return task.Result; }
}

但是我现在如何实现一个任务,比如说,等待 5 秒,然后返回一些字符串,例如“Hello World”?

一种方法是像这样直接使用Task:

Task<string> task = TaskEx.Run(
            () =>
                {
                    Thread.Sleep(5000);
                    return "Hello World";
                });

        string str = await task;

但是我将如何使用可等待的实现来做到这一点?还是我误解了一切?

感谢您提供任何信息/帮助:)

【问题讨论】:

  • async/await 适用于方法,不适用于任务。
  • @Henk await 适用于具有合适的GetAwaiter() 实现的表达式,Task 确实如此 - 所以await 适用于Task(在某种意义上)
  • 在这种狭义的情况下,您可以使用await TaskEx.Delay(5000);,但您必须针对一般情况进行更多研究。 await 关键字查找GetAwaiter,基本上任何提供合适GetAwaiter 的东西都可以与await 一起使用。 Jon Skeet 在他的博客中有一个很棒的系列,非常详细地剖析等待。
  • @HenkHolterman,await 不适用于方法。它适用于计算结果为 Task 或 Task(T) 的任何表达式。这是一个常见的错误。

标签: c# async-ctp async-await


【解决方案1】:

这里的关键是AsyncCtpThreadingExtensions.GetAwaiter,它通过扩展方法提供这些方法。由于异步实现是基于 pattern 的(如 LINQ),而不是绑定到特定接口,它可以来自任何地方(在这种情况下是 TaskAwaiter)。

您编写的代码是可等待的。例如:

static void Main()
{
    Test();
    Console.ReadLine(); // so the exe doesn't burninate
}
static async void Test() {
    Task<string> task = TaskEx.Run(
           () =>
           {
               Thread.Sleep(5000);
               return "Hello World";
           });
    string str = await task;
    Console.WriteLine(str);
}

这会在 5 秒后打印出Hello World

【讨论】:

  • 那么如何使用 GetAwaiter 实现一个类呢?
  • @Pajci 从头开始​​?我在此here 上发表了博客,并在 BookSleeve 源代码中留下了一个示例,但我soon concluded 认为最好使用 TaskCompletionSource - 预先存在的 比我的版本。
【解决方案2】:

一年后添加

在使用 async-await 一年多之后,我知道我在原始答案中写的关于 async 的一些内容是不正确的,尽管答案中的代码仍然是正确的。 Hera 是两个链接,极大地帮助了我理解 async-await 的工作原理。

This interview Eric Lippert shows an excellent analogy for async-await。在中间某处搜索 async-await。

In this article, the ever so helpful Eric Lippert shows some good practices for async-await

原答案

好的,这是一个在学习过程中帮助我的完整示例。

假设您有一个速度很慢的计算器,并且您想在按下按钮时使用它。同时,您希望您的 UI 保持响应,甚至可以做其他事情。计算器完成后,您希望显示结果。

当然:为此使用 async / await,而不要使用任何旧方法,例如设置事件标志和等待设置这些事件。

这是慢速计算器:

private int SlowAdd(int a, int b)
{
    System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5));
    return a+b;
}

如果你想在使用 async-await 时异步使用它,你必须使用 Task.Run(...) 来异步启动它。 Task.Run 的返回值是一个等待任务:

  • Task如果你运行的函数的返回值为void
  • Task&lt;TResult&gt; 如果你运行的函数的返回值为TResult

您可以只启动任务,执行其他操作,并在需要任务结果时键入 await。有一个缺点:

如果您想“等待”,您的函数需要异步并返回 Task 而不是 voidTask&lt;TResult&gt; 而不是 TResult

这是运行慢速计算器的代码。 使用 async 终止异步函数的标识符是一种常见的做法。

private async Task<int> SlowAddAsync(int a, int b)
{
    var myTask = Task.Run ( () => SlowAdd(a, b));
    // if desired do other things while the slow calculator is working
    // whenever you have nothing to do anymore and need the answer use await
    int result = await myTask;
    return result;
}

旁注:有些人更喜欢 Task.Factory.StartNew 而不是 Start.Run。看看 MSDN 是怎么说的:

MSDN: Task.Run versus Task.Factory.StartNew

SlowAdd 作为异步函数启动,您的线程继续运行。一旦它需要答案,它就会等待任务。返回值为 TResult,在本例中为 int。

如果你没有什么有意义的事情要做,代码看起来像这样:

private async Task`<int`> SlowAddAsync(int a, int b)
{
    return await Task.Run ( () => SlowAdd(a, b));
}

注意,SlowAddAsync 被声明为异步函数,所以使用这个异步函数的每个人也应该是异步的,并返回 Task 或 Task&lt;TResult>:

private async Task UpdateForm()
{
     int x = this.textBox1.Text;
     int y = this.textBox2.Text;
     int sum = await this.SlowAddAsync(x, y);
     this.label1.Text = sum.ToString();
}

关于 async / await 的好处是您不必摆弄 ContinueWith 来等待前一个任务完成。只需使用 await,您就知道任务已完成并且您有返回值。 await 之后的语句是您通常在 ContinueWith 中执行的操作。

顺便说一句,你的Task.Run不必调用函数,你也可以在里面放一个语句块:

int sum = await Task.Run( () => {
    System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5));
    return a+b});

然而,一个单独的函数的好处是你让那些不需要/想要/理解异步的人,可以在没有异步/等待的情况下使用该函数。

记住:

每个使用 await 的函数都应该是异步的

每个异步函数都应该返回 Task 或 Task&lt;Tresult>

“但我的事件处理程序无法返回任务!”

private void OnButton1_Clicked(object sender, ...){...}

你是对的,因此这是唯一的例外:

异步事件处理程序可能返回 void

因此,当单击按钮时,异步事件处理程序将使 UI 保持响应:

private async void OnButton1_Clicked(object sender, ...)
{
    await this.UpdateForm();
}

但是您仍然必须将事件处理程序声明为异步

许多 .NET 函数都有返回 Task 或 Task&lt;TResult> 的异步版本。

有异步函数 - 互联网 - 流式读写 - 数据库访问 - 等等。

要使用它们,您不必调用 Task.Run,​​它们已经返回 Task 和 Task&lt;TResult> 只需调用它们,继续做你自己的事情,当你需要答案时等待 Task 并使用 TResult .

开始几个任务并等待它们完成 如果您启动多个任务并希望等待所有任务完成,请使用 Task.WhenAll(...) NOT Task.Wait

Task.Wait 返回一个 void。 Task.WhenAll 返回一个任务,所以你可以等待它。

一旦一个任务完成,返回值已经是await的返回值,但是如果你await Task.WhenAll(new Task[]{TaskA,TaskB,TaskC}); 您必须使用 Task&lt;TResult>.Result 属性才能知道任务的结果:

int a = TaskA.Result;

如果其中一项任务引发异常,则会将其作为 InnerExceptions 包装在 AggregateException 中。因此,如果您等待 Task.WhenAll,请准备好捕获 AggregateException 并检查 innerExceptions 以查看您启动的任务引发的所有异常。使用 AggregateException.Flatten 函数可以更轻松地访问异常。

关于取消的有趣阅读:

MSDN about Cancellation in managed threads

最后:您使用 Thread.Sleep(...)。异步版本是 Task.Delay(TimeSpan):

private async Task`<int`> MySlowAdd(int a, int b)
{
    await Task.Delay(TimeSpan.FromSeconds(5));
    return a+b;
}

如果您使用此功能,您的程序将保持响应。

【讨论】:

    【解决方案3】:

    我最终得到了这个示例代码......这是等待模式的正确实现吗?

    namespace CTP_Testing
    {
    using System;
    using System.Linq;
    using System.Net;
    using System.Threading;
    using System.Threading.Tasks;
    
    public class CustomAsync
    {
        public static CustomAwaitable GetSiteHeadersAsync(string url)
        {
            return new CustomAwaitable(url);
        }
    }
    
    public class CustomAwaitable
    {
        private readonly Task<string> task;
        private readonly SynchronizationContext ctx;
    
        public CustomAwaitable(string url)
        {
            ctx = SynchronizationContext.Current;
            this.task = Task.Factory.StartNew(
                () =>
                    {
                        var req = (HttpWebRequest)WebRequest.Create(url);
                        req.Method = "HEAD";
                        var resp = (HttpWebResponse)req.GetResponse();
                        return this.FormatHeaders(resp.Headers);
                    });
        }
        public CustomAwaitable GetAwaiter() { return this; }
        public bool IsCompleted { get { return task.IsCompleted; } }
        public void OnCompleted(Action continuation)
        {
            task.ContinueWith(_ => ctx.Post(delegate { continuation(); }, null));
        }
        public string GetResult() { return task.Result; }
    
        private string FormatHeaders(WebHeaderCollection headers)
        {
            var headerString = headers.Keys.Cast<string>().Select(
                item => string.Format("{0}: {1}", item, headers[item]));
    
            return string.Join(Environment.NewLine, headerString.ToArray());
        }
    }
    

    【讨论】:

    • 使用 Async CTP 异步等待 Web 响应比这简单得多。提示:使用Task.Factory.FromAsync,或查找名称为xxxTaskAsync的方法。
    • 但这是等待模式的正确实现吗?
    • 我认为你的等待者需要实现 BeginAwait 和 EndAwait。你真的应该阅读 Jon Skeet 关于自定义等待者的博客系列,因为他在编写它们时提到了大量的陷阱。
    • 是的,BeginAwait 和 EndAwait 是在旧版本的 CTP 中......在新版本中,等待者模式发生了变化。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多