如何以及何时使用“异步”和“等待”


1064

根据我的理解asyncawait要做的主要事情之一就是使代码易于编写和阅读-但是使用它们是否等同于产生后台线程来执行长时间逻辑?

我目前正在尝试最基本的示例。我在行中添加了一些评论。你能为我澄清一下吗?

// I don't understand why this method must be marked as `async`.
private async void button1_Click(object sender, EventArgs e)
{
    Task<int> access = DoSomethingAsync();
    // task independent stuff here

    // this line is reached after the 5 seconds sleep from 
    // DoSomethingAsync() method. Shouldn't it be reached immediately? 
    int a = 1; 

    // from my understanding the waiting should be done here.
    int x = await access; 
}

async Task<int> DoSomethingAsync()
{
    // is this executed on a background thread?
    System.Threading.Thread.Sleep(5000);
    return 1;
}

48
另外,在您的示例中,请注意,在编译上述代码时会收到警告。注意警告。它告诉您该代码没有意义。
埃里克·利珀特

Answers:


759

使用时asyncawait编译器会在后台生成状态机。

这是一个示例,我希望我可以解释正在发生的一些高级细节:

public async Task MyMethodAsync()
{
    Task<int> longRunningTask = LongRunningOperationAsync();
    // independent work which doesn't need the result of LongRunningOperationAsync can be done here

    //and now we call await on the task 
    int result = await longRunningTask;
    //use the result 
    Console.WriteLine(result);
}

public async Task<int> LongRunningOperationAsync() // assume we return an int from this long running operation 
{
    await Task.Delay(1000); // 1 second delay
    return 1;
}

好,那么这里发生了什么:

  1. Task<int> longRunningTask = LongRunningOperationAsync(); 开始执行 LongRunningOperation

  2. 假设已经到达主线程(线程ID = 1),就完成了独立的工作await longRunningTask

    现在,如果longRunningTask尚未完成并且仍在运行,MyMethodAsync()将返回其调用方法,因此主线程不会被阻塞。当longRunningTask随后完成从线程池线程(可以是任何线程)将返回MyMethodAsync()其先前的背景下,继续执行(在这种情况下,打印结果到控制台)。

第二种情况是longRunningTask已经完成了执行并且结果可用。达到时,await longRunningTask我们已经有了结果,因此代码将继续在同一线程上执行。(在这种情况下,将结果打印到控制台)。当然,上面的例子并不是这样,其中Task.Delay(1000)涉及到。


65
为什么我们要对“ Task.Delay(1000);”进行“等待”?在LongRunningOperation异步方法中?
Benison Sam

3
@codea在Eric Lippert对文章的评论中,他将介绍性文章链接到该主题,他在其中专门将DoEvents策略与async-await进行了比较
Camilo Martinez

13
@BenisonSam线程有点旧,但是我有同样的问题,一直在寻找答案。“等待”的原因是,如果我们省略“等待”,则LongRunningOperationAsync()将立即返回。实际上,如果我们删除等待,编译器会发出警告。Stephen Cleary的博客文章blog.stephencleary.com/2011/09/…提供了出色的设计讨论。
shelbypereira 2015年

70
如果每个异步方法都需要在其中包含一个await,而await只能在具有async的方法上进行,那么何时停止?
布鲁诺·桑托斯

108
这个答案显然是错误的。这些许多投票将引起许多用户错误的理解。MS文档明确指出,仅在使用异步,等待时不使用其他线程。msdn.microsoft.com/zh-cn/library/mt674882.aspx请有人纠正答案。因此,我浪费了一整天。
克里希纳·迪帕克

171

根据我的理解,异步和等待要做的主要事情之一就是使代码易于编写和阅读。

它们是为了使异步代码易于编写和读取。

执行长时间逻辑与生成后台线程是否一样?

一点也不。

//我不明白为什么必须将此方法标记为“异步”。

async关键字使await关键字。因此,await必须标明使用的任何方法async

//从DoSomethingAsync()方法进入休眠状态5秒钟后,到达此行。不应该立即到达吗?

否,因为async默认情况下方法不在另一个线程上运行。

//这是在后台线程上执行的吗?

没有。


您可能会发现我的async/ await介绍很有帮助。该官方MSDN文档也异常丰厚(尤其是TAP部分),以及async团队做了一个很好的常见问题


6
因此它不在后台线程上运行,但也不会阻塞。由于异步API使用回调而不是使用线程,因此这是可能的。您启动(I / O,套接字等)操作,然后返回执行您的操作。操作完成后,操作系统将调用回调。这就是Node.js或Python Twisted框架所做的事情,它们也有一些不错的解释。
RomanPlášil13年

3
“ async关键字启用await关键字。因此,任何使用await的方法都必须标记为async。”,但是为什么呢?这个答案无助于理解为什么该方法必须标记为异步。编译器是否无法通过在内部查找await关键字来推断该方法是异步的?
斯坦尼斯拉夫

9
@Stanislav:我有一个博客条目可以解决这个问题。
Stephen Cleary 2014年

3
建议的说明:否,因为async默认情况下方法不在另一个线程上运行。在您的示例中,“ Sleep()内部” 调用会DoSomethingAsync()阻止当前线程,从而阻止执行继续进行button1_Click()直到DoSomethingAsync()完成。请注意,虽然Thread.Sleep()阻塞了执行线程,但Task.Delay() does not.
DavidRR

166

说明

这是async/ 的简要示例await。除此之外,还有很多其他细节需要考虑。

注意:Task.Delay(1000)模拟工作1秒钟。我认为最好将其视为等待外部资源的响应。由于我们的代码正在等待响应,因此系统可以将正在运行的任务放在一边,待完成后再返回。同时,它可以在该线程上执行其他一些工作。

在下面的示例中,第一个块正是这样做的。它立即启动所有任务(Task.Delay行),并将它们放到一边。代码将在该await a行上暂停,直到完成1秒的延迟,然后再转到下一行。因为bcd,和e一切开始在几乎相同的时间作为执行a(由于缺乏的await的),他们应该大致在这种情况下同时完成。

在下面的示例中,第二个块正在启动任务,并等待其完成(即完成await任务),然后再启动后续任务。每次迭代需要1秒。将await被暂停程序,并在继续之前等待结果。这是第一和第二块之间的主要区别。

Console.WriteLine(DateTime.Now);

// This block takes 1 second to run because all
// 5 tasks are running simultaneously
{
    var a = Task.Delay(1000);
    var b = Task.Delay(1000);
    var c = Task.Delay(1000);
    var d = Task.Delay(1000);
    var e = Task.Delay(1000);

    await a;
    await b;
    await c;
    await d;
    await e;
}

Console.WriteLine(DateTime.Now);

// This block takes 5 seconds to run because each "await"
// pauses the code until the task finishes
{
    await Task.Delay(1000);
    await Task.Delay(1000);
    await Task.Delay(1000);
    await Task.Delay(1000);
    await Task.Delay(1000);
}
Console.WriteLine(DateTime.Now);

输出:

5/24/2017 2:22:50 PM
5/24/2017 2:22:51 PM (First block took 1 second)
5/24/2017 2:22:56 PM (Second block took 5 seconds)

有关SynchronizationContext的其他信息

注意:这对我来说有点迷惑,因此,如果我在任何事情上都不对,请纠正我,我将更新答案。对它的工作原理有一个基本的了解很重要,但是只要您不使用它,就可以在不成为专家的情况下获得成功ConfigureAwait(false),尽管我认为您可能会失去一些优化机会。

这种情况的一方面使async/ await概念难以掌握。这是事实,在此示例中,所有操作都在同一线程上发生(或至少发生在同一线程上SynchronizationContext)。默认情况下,await将恢复运行该线程的原始线程的同步上下文。例如,在ASP.NET中HttpContext,当请求进入时,您将其绑定到线程。此上下文包含特定于原始Http请求的内容,例如原始Request对象,其具有语言,IP地址,标头等内容。如果在处理某些内容的过程中切换了线程,则可能最终会尝试从另一个对象上的该对象中提取信息。HttpContext这可能是灾难性的。如果您知道将不会使用上下文,则可以选择“不在乎”。基本上,这使您的代码可以在单独的线程上运行,而不会带来上下文。

您如何实现的?默认情况下,await a;代码实际上假设您确实要捕获并还原上下文:

await a; //Same as the line below
await a.ConfigureAwait(true);

如果要允许主代码在没有原始上下文的情况下在新线程上继续运行,则只需使用false而不是true,这样它就知道不需要还原上下文。

await a.ConfigureAwait(false);

该计划完成被暂停后,它将继续有可能在一个完全不同的线程具有不同的背景。这就是性能提高的源泉-它可以在任何可用线程上继续运行,而不必还原其开始时使用的原始上下文。

这些东西令人困惑吗?真是的!你能弄清楚吗?大概!掌握了这些概念之后,请继续阅读Stephen Cleary的解释,这些解释往往更倾向于对async/ await已经具有技术理解的人。


可以说,如果所有这些任务都返回一个int值,并且如果我在第二个任务(或某些计算)中使用第一个任务的结果,那会错吗?
veerendra gupta

3
@veerendragupta是的。在这种情况下,您将有意识地选择不异步运行它们(因为它们不是异步的)。关于配置上下文,还有其他一些要实现的事情,我将不在这里介绍
Joe Phillips

await MethodCall()是绝对的浪费吗?您不妨删除await/ async吗?
维塔尼'18

2
@Jocie不完全是。当您调用时await,我认为它会将线程释放回池中而不是将其保留。这使得它可以在等待任务返回时在其他地方使用
Joe Phillips

2
@JoePhillips我认为您刚才说的是异步/等待的本质。调用线程已释放,并且可以由计算机上的其他进程使用。等待调用完成后,将使用新线程来恢复调用方最初启动的操作。调用方仍在等待,但是好处是在此期间释放了一个线程。那是异步/等待的好处?
Bob Horn

147

进一步了解其他答案,看看await(C#参考)

更具体地讲,在所包含的示例中,它会稍微说明您的情况

以下Windows窗体示例说明了在异步方法WaitAsynchronouslyAsync中使用await。将该方法的行为与WaitSynchronously的行为进行对比。没有将await运算符应用于任务,尽管在其定义中使用了async修饰符并在其主体中调用了Thread.Sleep,但WaitSynchronously仍同步运行。

private async void button1_Click(object sender, EventArgs e)
{
    // Call the method that runs asynchronously.
    string result = await WaitAsynchronouslyAsync();

    // Call the method that runs synchronously.
    //string result = await WaitSynchronously ();

    // Display the result.
    textBox1.Text += result;
}

// The following method runs asynchronously. The UI thread is not
// blocked during the delay. You can move or resize the Form1 window 
// while Task.Delay is running.
public async Task<string> WaitAsynchronouslyAsync()
{
    await Task.Delay(10000);
    return "Finished";
}

// The following method runs synchronously, despite the use of async.
// You cannot move or resize the Form1 window while Thread.Sleep
// is running because the UI thread is blocked.
public async Task<string> WaitSynchronously()
{
    // Add a using directive for System.Threading.
    Thread.Sleep(10000);
    return "Finished";
}

3
感谢您的回答。但是,是否在单独的线程上执行WaitAsynchronouslyAsync()?
Dan Dinu

32
我确实如此,从“ await表达式
Adriaan Stander

13
根据此MSDN文章,“ async和await关键字不会导致创建其他线程...。async方法不会在其自己的线程上运行”。我的理解是,在等待关键字时,框架会跳过(返回到调用者)以允许所有可能的独立代码在等待较长的操作完成时运行。我认为这意味着一旦所有独立代码运行完毕,如果长操作没有返回,它将阻塞。我现在只是在学习这个。
Vimes 2013年

9
@astander这是不正确的。它并没有在不同的线程中执行。它只是安排在Task.Delay触发使用的计时器时调用继续(方法的其余部分)。
MgSam

1
由于睡眠,此答案是错误的。请使用await Task.Delay(1000)查看已接受的答案;具有正确的行为。
Jared Updike 2014年

62

在一个简单的控制台程序中显示上述解释:

class Program
{
    static void Main(string[] args)
    {
        TestAsyncAwaitMethods();
        Console.WriteLine("Press any key to exit...");
        Console.ReadLine();
    }

    public async static void TestAsyncAwaitMethods()
    {
        await LongRunningMethod();
    }

    public static async Task<int> LongRunningMethod()
    {
        Console.WriteLine("Starting Long Running method...");
        await Task.Delay(5000);
        Console.WriteLine("End Long Running method...");
        return 1;
    }
}

输出为:

Starting Long Running method...
Press any key to exit...
End Long Running method...

从而,

  1. Main通过开始启动长期运行的方法TestAsyncAwaitMethods。这将立即返回,而不会停止当前线程,我们会立即看到“按任意键退出”消息
  2. 所有这一切,LongRunningMethod在后台运行。完成后,来自Threadpool的另一个线程将拾取此上下文并显示最终消息

因此,没有线程被阻塞。


在输出的哪一部分中会显示“按任意键退出...”?
StudioX

1
(返回1)的用途是什么?有必要吗?
StudioX

1
@StudioX我认为它必须具有返回类型整数
Kuba Do

我认为该return 1部分值得进一步说明:该await关键字允许您直接返回其基础类型Task<T>,从而使退出代码更容易适应await / async世界。但是您不必返回值,因为可以在Task不指定返回类型的情况下返回,这与同步void方法等效。请注意,C#允许使用async void方法,但除非处理事件处理程序,否则应避免这样做。
克里斯蒂安诺

41

我认为您选择了一个不好的例子 System.Threading.Thread.Sleep

一个点async的任务就是让它在后台执行不锁定主线程,比如做一个DownloadFileAsync

System.Threading.Thread.Sleep 不是“完成”的事情,它只是睡觉,因此在5秒钟后到达下一行...

阅读本文,我认为这是一个很好的解释asyncawait概念:http : //msdn.microsoft.com/zh-cn/library/vstudio/hh191443.aspx


3
为什么睡眠是不好的例子,而下载是很好的例子。当我看到Thread.Sleep时,就像FooBar之类的东西,我知道有些任务需要花费时间。我认为他的问题很重要
Abdurrahim

1
@Abdurrahim会Thread.Sleep阻塞线程(该线程只能闲置,无法执行其他任何操作),但异步方法则不会。在的情况下DownloadFileAsync,线程可以继续执行其他操作,直到来自远程服务器的答复为止。在异步方法中Task.Delay,“某些需要时间的任务”更好的占位符是,因为它实际上是异步的。
加布里埃尔·卢西(Jabry Luci)

@GabrielLuci我的反对不是关于延迟与睡眠;您的答案看起来更像是稻草人的答案。如果您将其作为对这个问题的评论,我什么都不会反对,但作为答案,它的味道更像是草人的答案。我认为即使在他/她必须进行的所有呼叫都将阻止呼叫的情况下,也可以在该处使用异步;它不会使所有目的无效……甚至所有剩下的都是句法糖,它算作有效案例,
Abdurrahim

1
这不是我的答案。但是要解决您的观点:这取决于方法的目的。如果他只想调用一个方法,他就成功了。但是在这种情况下,他试图创建一种异步运行的方法。他只是通过使用async关键字来做到这一点。但是他的方法仍然是同步运行的,这个答案很好地解释了原因:因为他实际上没有运行任何异步代码。标记的方法async仍会同步运行,直到您await不完整为止Task。如果没有await,则该方法将同步运行,编译器会警告您。
加布里埃尔·卢西(Jabry Luci)

23

这是一个快速的控制台程序,它可以使后续操作者更容易理解。该TaskToDo方法是您要使其异步的长期运行的方法。使它异步运行是由该TestAsync方法完成的。测试循环方法仅运行TaskToDo任务并异步运行它们。您可以在结果中看到这一点,因为它们在每次运行中的执行顺序都不相同-它们在完成时向控制台UI线程报告。简单化,但我认为简单化的示例比涉及更多的示例更好地展现了模式的核心:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace TestingAsync
{
    class Program
    {
        static void Main(string[] args)
        {
            TestLoops();
            Console.Read();
        }

        private static async void TestLoops()
        {
            for (int i = 0; i < 100; i++)
            {
                await TestAsync(i);
            }
        }

        private static Task TestAsync(int i)
        {
            return Task.Run(() => TaskToDo(i));
        }

        private async static void TaskToDo(int i)
        {
            await Task.Delay(10);
            Console.WriteLine(i);
        }
    }
}

20

为了最快的学习。

  • 了解方法执行流程(带有图表):3分钟

  • 问题自省(学习上的缘故):1分钟

  • 快速了解语法糖:5分钟

  • 分享开发者的困惑:5分钟

  • 问题:将真实代码的实际实现快速更改为异步代码:2分钟

  • 下一步要去哪里?

了解方法执行流程(带有图表):3分钟

在此图像中,仅关注#6(仅此而已) 在此处输入图片说明

在#6步骤:执行已在这里用尽,因为它已经用完了。要继续,它需要getStringTask(一种函数)的结果。因此,它使用await运算符来暂停其进度并将控制权交还给调用方(我们正在使用的此方法)。最初在#2中进行了对getStringTask的实际调用。在#2处承诺返回字符串结果。但是什么时候返回结果呢?我们(#1:AccessTheWebAsync)是否应该再次拨打第二个电话?谁得到结果,#2(调用语句)或#6(等待语句)

AccessTheWebAsync()的外部调用方也正在等待。因此,调用方正在等待AccessTheWebAsync,而AccessTheWebAsync目前正在等待GetStringAsync。有趣的是AccessTheWebAsync在等待之前进行了一些工作(#4),也许是为了节省等待时间。外部呼叫者(和链中的所有呼叫者)也享有相同的多任务自由度,这是“异步”事情的最大优点!您觉得它是同步的..或正常的,但不是。

请记住,该方法已经返回(#2),它无法再次返回(没有第二次)。那么呼叫者怎么知道?一切都与任务有关任务已通过。等待任务(不是方法,不是值)。值将在“任务”中设置。任务状态将设置为完成。调用方仅监视Task(#6)。所以6#是哪里/谁得到结果的答案。进一步的阅读在这里稍后。

为了学习起见,对问题进行内省:1分钟

让我们稍微调整一下问题:

如何以及何时使用and asyncawait Tasks

因为学习Task自动涵盖了另外两个(并回答了您的问题)

快速了解语法糖:5分钟

  • 转换前(原始方法)

    internal static int Method(int arg0, int arg1) { int result = arg0 + arg1; IO(); // Do some long running IO. return result; }

  • 任务定义方法调用上述方法

    internal static Task<int> MethodTask(int arg0, int arg1) { Task<int> task = new Task<int>(() => Method(arg0, arg1)); task.Start(); // Hot task (started task) should always be returned. return task; }

我们提到了等待还是异步?否。调用上述方法,您将获得一个可以监视的任务。您已经知道任务返回什么。整数。

  • 调用Task有点棘手,也就是关键字开始出现时。让我们调用MethodTask()

    internal static async Task<int> MethodAsync(int arg0, int arg1) { int result = await HelperMethods.MethodTask(arg0, arg1); return result; }

上面的相同代码如下图所示: 在此处输入图片说明

  1. 我们正在“等待”完成任务。因此await
  2. 由于我们使用等待,因此必须使用async(强制语法)
  3. 以MethodAsync Async为前缀(编码标准)

await很容易理解,但其余两个(asyncAsync)可能不是:)。那么,它应该使很多更有意义的编译器后though.Further读取这里

因此,共有两个部分。

  1. 创建“任务”
  2. 创建语法糖来调用任务(await+async

记住,我们有一个AccessTheWebAsync()的外部调用者,并且该调用者也没有幸免...也就是说,它也需要相同的调用者await+async。和链继续。但是总会有Task一个结局。

很好,但是一位开发人员惊讶地发现缺少#1(任务)...

分享开发者的困惑:5分钟

开发人员犯了一个错误,即不执行,Task但仍然可以使用!尝试理解问题,并仅提供此处提供的可接受的答案。希望您已经阅读并完全理解。总结是我们可能看不到/没有实现“任务”,但是它是在父类的某个地方实现的。同样,在我们的示例中,调用一个已构建MethodAsync()的方法比使用我们自己的TaskMethodTask())方法更容易。大多数开发人员发现,很难Tasks将代码转换为异步代码。

提示:尝试查找现有的Async实现(例如MethodAsyncToListAsync)以将困难外包。因此,我们只需要处理Async和await(这很简单,与普通代码非常相似)

问题:将真实代码的实际实现快速更改为异步操作:2分钟

下面在数据层中显示的代码行开始中断(很多地方)。因为我们将某些代码从.Net framework 4.2。*更新到了.Net core。我们必须在整个应用程序中在1小时内解决此问题!

var myContract = query.Where(c => c.ContractID == _contractID).First();

十分简单!

  1. 我们安装了EntityFramework nuget软件包,因为它具有QueryableExtensions。或者换句话说它的异步执行(任务),所以我们可以用简单的生存Asyncawait代码。
  2. 命名空间= Microsoft.EntityFrameworkCore

调用代码行像这样更改

var myContract = await query.Where(c => c.ContractID == _contractID).FirstAsync();
  1. 方法签名从更改为

    Contract GetContract(int contractnumber)

    async Task<Contract> GetContractAsync(int contractnumber)

  2. 调用方法也受到影响:GetContractAsync(123456);被称为GetContractAsync(123456).Result;

  3. 我们在30分钟内到处更改了它!

但是架构师告诉我们不要为此使用EntityFramework库!哎呀!戏剧!然后,我们做了一个自定义的Task实现(yuk)。你知道的。还是容易!..still yuk ..

下一步要去哪里? 我们可以观看一段精彩的快速视频,介绍如何在ASP.Net Core中将同步调用转换为异步,这可能是阅读此书后的发展方向。


很棒的答案!
这帮

1
好答案。您可能只想修复一些小问题,例如:(a)提到“ .Net framework 4.2”(我不知道存在该版本)(b)EntityFrameWork => EntityFramework中的框
immitev

15

这里的所有答案都使用Task.Delay()或其他一些内置async函数。但是这是我的示例,这些示例都不使用任何async功能:

// Starts counting to a large number and then immediately displays message "I'm counting...". 
// Then it waits for task to finish and displays "finished, press any key".
static void asyncTest ()
{
    Console.WriteLine("Started asyncTest()");
    Task<long> task = asyncTest_count();
    Console.WriteLine("Started counting, please wait...");
    task.Wait(); // if you comment this line you will see that message "Finished counting" will be displayed before we actually finished counting.
    //Console.WriteLine("Finished counting to " + task.Result.ToString()); // using task.Result seems to also call task.Wait().
    Console.WriteLine("Finished counting.");
    Console.WriteLine("Press any key to exit program.");
    Console.ReadLine();
}

static async Task<long> asyncTest_count()
{
    long k = 0;
    Console.WriteLine("Started asyncTest_count()");
    await Task.Run(() =>
    {
        long countTo = 100000000;
        int prevPercentDone = -1;
        for (long i = 0; i <= countTo; i++)
        {
            int percentDone = (int)(100 * (i / (double)countTo));
            if (percentDone != prevPercentDone)
            {
                prevPercentDone = percentDone;
                Console.Write(percentDone.ToString() + "% ");
            }

            k = i;
        }
    });
    Console.WriteLine("");
    Console.WriteLine("Finished asyncTest_count()");
    return k;
}

2
谢谢!第一个实际起作用而不是等待的答案。
Jeffnl

感谢您的展示,task.Wait();以及如何使用它避免异步/等待地狱:P
编码器

12

该答案旨在提供一些特定于ASP.NET的信息。

通过在MVC控制器中使用async / await,可以提高线程池利用率并实现更好的吞吐量,如以下文章所述,

http://www.asp.net/mvc/tutorials/mvc-4/using-asynchronous-methods-in-aspnet-mvc-4

在启动时看到大量并发请求或负载突增(并发性突然增加)的Web应用程序中,使这些Web服务调用异步可以提高应用程序的响应能力。异步请求与同步请求所花费的时间相同。例如,如果请求进行Web服务调用需要两秒钟才能完成,那么无论是同步还是异步执行,该请求都将花费两秒钟。但是,在异步调用期间,在等待第一个请求完成时,不会阻止线程响应其他请求。因此,当有许多并发请求调用长时间运行的操作时,异步请求会阻止请求排队和线程池增长。


12

异步并等待简单说明

简单类比

一个人可以等待早上的火车。这就是他们正在做的所有事情,因为这是他们当前正在执行的主要任务。(同步编程(您通常会做什么!))

另一个人可能在等待早上的火车上,一边吸烟,一边喝咖啡。(异步编程)

什么是异步编程?

异步编程是程序员选择在与执行主线程不同的线程上运行其某些代码,然后在完成时通知主线程的地方。

async关键字实际上有什么作用?

将async关键字前缀为方法名称,例如

async void DoSomething(){ . . .

允许程序员在调用异步任务时使用await关键字。这就是全部。

为什么这很重要?

在许多软件系统中,主线程被保留用于与用户界面有关的特定操作。如果我正在计算机上运行一个非常复杂的递归算法,该算法需要5秒才能完成,但是我在主线程(UI线程)上运行此算法,则当用户尝试单击我的应用程序中的任何内容时,它似乎被冻结了。因为我的主线程已排队,并且当前正在处理太多操作。结果,主线程无法处理鼠标单击以从按钮单击运行该方法。

什么时候使用异步和等待?

在执行不涉及用户界面的任何操作时,最好使用异步关键字。

因此,可以说您正在编写一个程序,该程序允许用户在手机上绘制草图,但是每5秒钟它将检查一次互联网上的天气。

由于应用程序的用户需要与移动触摸屏保持互动以绘制漂亮的图片,因此我们应该等待每5秒钟轮询一次的呼叫以获取天气信息。

您如何使用异步和等待

在上面的示例之后,这是一些如何编写它的伪代码:

    //ASYNCHRONOUS
    //this is called using the await keyword every 5 seconds from a polling timer or something.

    async Task CheckWeather()
    {
        var weather = await GetWeather();
        //do something with the weather now you have it
    }

    async Task<WeatherResult> GetWeather()
    {

        var weatherJson = await CallToNetworkAddressToGetWeather();
        return deserializeJson<weatherJson>(weatherJson);
    }

    //SYNCHRONOUS
    //This method is called whenever the screen is pressed
    void ScreenPressed()
    {
        DrawSketchOnScreen();
    }

附加说明-更新

我忘了在原始注释中提到,在C#中,您只能等待Tasks中包装的方法。例如,您可以等待此方法:

// awaiting this will return a string.
// calling this without await (synchronously) will result in a Task<string> object.
async Task<string> FetchHelloWorld() {..

您不能等待不是这样的任务的方法:

async string FetchHelloWorld() {..

在此处随意查看Task类的源代码。


4
感谢您抽出宝贵的时间来撰写此文章。
Prashant

10

异步/等待

实际上,异步/等待是一对关键字,它们只是用于创建异步任务的回调的语法糖。

以该操作为例:

    public static void DoSomeWork()
    {
        var task = Task.Run(() =>
        {
            // [RUNS ON WORKER THREAD]

            // IS NOT bubbling up due to the different threads
            throw new Exception();
            Thread.Sleep(2000);

            return "Hello";
        });

        // This is the callback
        task.ContinueWith((t) => {
            // -> Exception is swallowed silently
            Console.WriteLine("Completed");

            // [RUNS ON WORKER THREAD]
        });
    }

上面的代码有几个缺点。错误不会传递,很难读取。但是Async和Await可以帮助我们:

    public async static void DoSomeWork()
    {
        var result = await Task.Run(() =>
        {
            // [RUNS ON WORKER THREAD]

            // IS bubbling up
            throw new Exception();
            Thread.Sleep(2000);

            return "Hello";
        });

        // every thing below is a callback 
        // (including the calling methods)

        Console.WriteLine("Completed");

    }

等待调用必须在Async方法中。这具有一些优点:

  • 返回任务的结果
  • 自动创建回调
  • 检查错误并让它们在调用堆栈中冒泡(仅在调用堆栈中最多等待呼叫)
  • 等待结果
  • 释放主线程
  • 在主线程上运行回调
  • 使用线程池中的工作线程执行任务
  • 使代码易于阅读
  • 还有更多

注意:Async和Await 异步调用一起使用不要使用它们。为此,您必须使用Task Libary,例如Task.Run()。

这是等待和不等待解决方案之间的比较

这是非异步解决方案:

    public static long DoTask()
    {
        stopWatch.Reset();
        stopWatch.Start();

        // [RUNS ON MAIN THREAD]
        var task = Task.Run(() => {
            Thread.Sleep(2000);
            // [RUNS ON WORKER THREAD]
        });
        // goes directly further
        // WITHOUT waiting until the task is finished

        // [RUNS ON MAIN THREAD]

        stopWatch.Stop();
        // 50 milliseconds
        return stopWatch.ElapsedMilliseconds;
    }

这是异步方法:

    public async static Task<long> DoAwaitTask()
    {
        stopWatch.Reset();
        stopWatch.Start();

        // [RUNS ON MAIN THREAD]

        await Task.Run(() => {
            Thread.Sleep(2000);
            // [RUNS ON WORKER THREAD]
        });
        // Waits until task is finished

        // [RUNS ON MAIN THREAD]

        stopWatch.Stop();
        // 2050 milliseconds
        return stopWatch.ElapsedMilliseconds;
    }

您实际上可以在没有await关键字的情况下调用异步方法,但这意味着此处的任何异常都在发布模式下被吞噬:

    public static Stopwatch stopWatch { get; } = new Stopwatch();

    static void Main(string[] args)
    {
        Console.WriteLine("DoAwaitTask: " + DoAwaitTask().Result + " ms");
        // 2050 (2000 more because of the await)
        Console.WriteLine("DoTask: " + DoTask() + " ms");
        // 50
        Console.ReadKey();
    }

Async和Await不适用于并行计算。它们用于不阻塞您的主线程。当涉及到asp.net或Windows应用程序时,由于网络调用而阻塞主线程是一件坏事。如果这样做,您的应用程序将无响应甚至崩溃。

查阅ms docs以获取更多示例。


9

老实说,我仍然认为最好的解释是关于维基百科上关于未来和承诺的解释:http : //en.wikipedia.org/wiki/Futures_and_promises

基本思想是您有一个单独的线程池,这些线程池异步执行任务。使用时。但是,该对象确实保证会在某个时间执行该操作,并在您请求时为您提供结果。这意味着它将在您请求结果并且尚未完成时阻塞,否则将在线程池中执行。

您可以从那里进行优化:可以异步实现某些操作,还可以通过将后续请求组合在一起和/或重新排序来优化文件IO和网络通信之类的功能。我不确定这是否已经存在于Microsoft的任务框架中-但如果不是,那将是我要添加的第一件事。

您实际上可以在C#4.0中使用yields实现将来的模式排序。如果您想知道它是如何工作的,我可以推荐此链接,它做得不错:http : //code.google.com/p/fracture/source/browse/trunk/Squared/TaskLib/。但是,如果您自己开始玩弄它,您会注意到,如果您想做所有很酷的事情,那么您确实需要语言支持-这正是Microsoft所做的。


8

请参阅此提琴https://dotnetfiddle.net/VhZdLU(并在可能的情况下进行改进),以运行一个简单的控制台应用程序,该应用程序显示Task,Task.WaitAll(),async和await的用法操作符在同一程序中的。

这个小提琴应该清除您的执行周期概念。

这是示例代码

using System;
using System.Threading.Tasks;

public class Program
{
    public static void Main()
    {               
        var a = MyMethodAsync(); //Task started for Execution and immediately goes to Line 19 of the code. Cursor will come back as soon as await operator is met       
        Console.WriteLine("Cursor Moved to Next Line Without Waiting for MyMethodAsync() completion");
        Console.WriteLine("Now Waiting for Task to be Finished");       
        Task.WaitAll(a); //Now Waiting      
        Console.WriteLine("Exiting CommandLine");       
    }

    public static async Task MyMethodAsync()
    {
        Task<int> longRunningTask = LongRunningOperation();
        // independent work which doesn't need the result of LongRunningOperationAsync can be done here
        Console.WriteLine("Independent Works of now executes in MyMethodAsync()");
        //and now we call await on the task 
        int result = await longRunningTask;
        //use the result 
        Console.WriteLine("Result of LongRunningOperation() is " + result);
    }

    public static async Task<int> LongRunningOperation() // assume we return an int from this long running operation 
    {
        Console.WriteLine("LongRunningOperation() Started");
        await Task.Delay(2000); // 2 second delay
        Console.WriteLine("LongRunningOperation() Finished after 2 Seconds");
        return 1;
    }   

}

跟踪来自输出窗口: 在此处输入图片说明


3
public static void Main(string[] args)
{
    string result = DownloadContentAsync().Result;
    Console.ReadKey();
}

// You use the async keyword to mark a method for asynchronous operations.
// The "async" modifier simply starts synchronously the current thread. 
// What it does is enable the method to be split into multiple pieces.
// The boundaries of these pieces are marked with the await keyword.
public static async Task<string> DownloadContentAsync()// By convention, the method name ends with "Async
{
    using (HttpClient client = new HttpClient())
    {
        // When you use the await keyword, the compiler generates the code that checks if the asynchronous operation is finished.
        // If it is already finished, the method continues to run synchronously.
        // If not completed, the state machine will connect a continuation method that must be executed WHEN the Task is completed.


        // Http request example. 
        // (In this example I can set the milliseconds after "sleep=")
        String result = await client.GetStringAsync("http://httpstat.us/200?sleep=1000");

        Console.WriteLine(result);

        // After completing the result response, the state machine will continue to synchronously execute the other processes.


        return result;
    }
}

3

在更高级别:

1)Async关键字启用了等待,仅此而已。Async关键字不会在单独的线程中运行该方法。开头的f async方法同步运行,直到它等待耗时的任务。

2)您可以等待返回Task或T类型Task的方法。您不能等待异步void方法。

3)当主线程遇到等待耗时的任务或开始实际工作时,主线程将返回当前方法的调用者。

4)如果主线程看到正在等待执行的任务正在等待,则它不会等待,而是返回到当前方法的调用方。这样,应用程序将保持响应状态。

5)等待处理任务,现在将在与线程池不同的线程上执行。

6)此等待任务完成后,其下面的所有代码将由单独的线程执行

下面是示例代码。执行它并检查线程ID

using System;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncAwaitDemo
{
    class Program
    {
        public static async void AsynchronousOperation()
        {
            Console.WriteLine("Inside AsynchronousOperation Before AsyncMethod, Thread Id: " + Thread.CurrentThread.ManagedThreadId);
            //Task<int> _task = AsyncMethod();
            int count = await AsyncMethod();

            Console.WriteLine("Inside AsynchronousOperation After AsyncMethod Before Await, Thread Id: " + Thread.CurrentThread.ManagedThreadId);

            //int count = await _task;

            Console.WriteLine("Inside AsynchronousOperation After AsyncMethod After Await Before DependentMethod, Thread Id: " + Thread.CurrentThread.ManagedThreadId);

            DependentMethod(count);

            Console.WriteLine("Inside AsynchronousOperation After AsyncMethod After Await After DependentMethod, Thread Id: " + Thread.CurrentThread.ManagedThreadId);
        }

        public static async Task<int> AsyncMethod()
        {
            Console.WriteLine("Inside AsyncMethod, Thread Id: " + Thread.CurrentThread.ManagedThreadId);
            int count = 0;

            await Task.Run(() =>
            {
                Console.WriteLine("Executing a long running task which takes 10 seconds to complete, Thread Id: " + Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(20000);
                count = 10;
            });

            Console.WriteLine("Completed AsyncMethod, Thread Id: " + Thread.CurrentThread.ManagedThreadId);

            return count;
        }       

        public static void DependentMethod(int count)
        {
            Console.WriteLine("Inside DependentMethod, Thread Id: " + Thread.CurrentThread.ManagedThreadId + ". Total count is " + count);
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Started Main method, Thread Id: " + Thread.CurrentThread.ManagedThreadId);

            AsynchronousOperation();

            Console.WriteLine("Completed Main method, Thread Id: " + Thread.CurrentThread.ManagedThreadId);

            Console.ReadKey();
        }

    }
}

2

我的理解也是,应该在组合中添加第三个术语:Task

Async 只是您在方法上使用的限定符,它表示这是一个异步方法。

Task是该async函数的返回。它异步执行。

await一个任务。当代码执行到达此行时,控件跳回到周围原始函数的调用方。

相反地,如果您分配的返回async功能(即Task)给一个变量,当代码执行到达此行,它只是继续过去那种线周围的功能,而Task执行异步。


1

使用它们是否等于生成后台线程来执行长时间逻辑?

本文MDSN:使用异步和等待(C#)的异步编程对此进行了明确说明:

async和await关键字不会导致创建其他线程。异步方法不需要多线程,因为异步方法不会在自己的线程上运行。该方法在当前同步上下文上运行,并且仅在该方法处于活动状态时才在线程上使用时间。


1

在以下代码中,HttpClient方法GetByteArrayAsync返回一个任务,即getContentsTask。该任务是在任务完成时产生实际字节数组的承诺。将await运算符应用于getContentsTask,以暂停SumPageSizesAsync中的执行,直到getContentsTask完成。同时,控制权返回给SumPageSizesAsync的调用方。当getContentsTask完成时,await表达式的值将为一个字节数组。

private async Task SumPageSizesAsync()
{
    // To use the HttpClient type in desktop apps, you must include a using directive and add a 
    // reference for the System.Net.Http namespace.
    HttpClient client = new HttpClient();
    // . . .
    Task<byte[]> getContentsTask = client.GetByteArrayAsync(url);
    byte[] urlContents = await getContentsTask;

    // Equivalently, now that you see how it works, you can write the same thing in a single line.
    //byte[] urlContents = await client.GetByteArrayAsync(url);
    // . . .
}

1

下面是通过打开对话框读取excel文件,然后使用async并等待运行异步的代码,该代码从excel一行一行地读取并绑定到网格

namespace EmailBillingRates
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            lblProcessing.Text = "";
        }

        private async void btnReadExcel_Click(object sender, EventArgs e)
        {
            string filename = OpenFileDialog();

            Microsoft.Office.Interop.Excel.Application xlApp = new Microsoft.Office.Interop.Excel.Application();
            Microsoft.Office.Interop.Excel.Workbook xlWorkbook = xlApp.Workbooks.Open(filename);
            Microsoft.Office.Interop.Excel._Worksheet xlWorksheet = xlWorkbook.Sheets[1];
            Microsoft.Office.Interop.Excel.Range xlRange = xlWorksheet.UsedRange;
            try
            {
                Task<int> longRunningTask = BindGrid(xlRange);
                int result = await longRunningTask;

            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message.ToString());
            }
            finally
            {
                //cleanup  
               // GC.Collect();
                //GC.WaitForPendingFinalizers();

                //rule of thumb for releasing com objects:  
                //  never use two dots, all COM objects must be referenced and released individually  
                //  ex: [somthing].[something].[something] is bad  

                //release com objects to fully kill excel process from running in the background  
                Marshal.ReleaseComObject(xlRange);
                Marshal.ReleaseComObject(xlWorksheet);

                //close and release  
                xlWorkbook.Close();
                Marshal.ReleaseComObject(xlWorkbook);

                //quit and release  
                xlApp.Quit();
                Marshal.ReleaseComObject(xlApp);
            }

        }

        private void btnSendEmail_Click(object sender, EventArgs e)
        {

        }

        private string OpenFileDialog()
        {
            string filename = "";
            OpenFileDialog fdlg = new OpenFileDialog();
            fdlg.Title = "Excel File Dialog";
            fdlg.InitialDirectory = @"c:\";
            fdlg.Filter = "All files (*.*)|*.*|All files (*.*)|*.*";
            fdlg.FilterIndex = 2;
            fdlg.RestoreDirectory = true;
            if (fdlg.ShowDialog() == DialogResult.OK)
            {
                filename = fdlg.FileName;
            }
            return filename;
        }

        private async Task<int> BindGrid(Microsoft.Office.Interop.Excel.Range xlRange)
        {
            lblProcessing.Text = "Processing File.. Please wait";
            int rowCount = xlRange.Rows.Count;
            int colCount = xlRange.Columns.Count;

            // dt.Column = colCount;  
            dataGridView1.ColumnCount = colCount;
            dataGridView1.RowCount = rowCount;

            for (int i = 1; i <= rowCount; i++)
            {
                for (int j = 1; j <= colCount; j++)
                {
                    //write the value to the Grid  
                    if (xlRange.Cells[i, j] != null && xlRange.Cells[i, j].Value2 != null)
                    {
                         await Task.Delay(1);
                         dataGridView1.Rows[i - 1].Cells[j - 1].Value =  xlRange.Cells[i, j].Value2.ToString();
                    }

                }
            }
            lblProcessing.Text = "";
            return 0;
        }
    }

    internal class async
    {
    }
}

0

此处的答案可用作有关等待/异步的一般指导。它们还包含有关如何连接等待/异步的一些详细信息。我想与您分享一些使用该设计模式之前应了解的实践经验。

术语“ await”是文字,因此无论您在其上调用什么线程,都将在继续执行之前等待方法的结果。在前台线程上,这是一场灾难。前台线程承担了构建应用程序的重担,其中包括视图,视图模型,初始动画以及使用这些元素进行引导的所有其他内容。因此,当您等待前台线程时,您将停止应用程序。用户等待,等待什么都没有发生。这提供了负面的用户体验。

您当然可以使用多种方法等待后台线程:

Device.BeginInvokeOnMainThread(async () => { await AnyAwaitableMethod(); });

// Notice that we do not await the following call, 
// as that would tie it to the foreground thread.
try
{
Task.Run(async () => { await AnyAwaitableMethod(); });
}
catch
{}

这些说明的完整代码位于https://github.com/marcusts/xamarin-forms-annoyances。请参阅称为AwaitAsyncAntipattern.sln的解决方案。

GitHub网站还提供了指向该主题的更详细讨论的链接。


1
据我了解,async / await是回调的语法糖,它与线程无关。msdn.microsoft.com/zh-cn/magazine/hh456401.aspx 用于非CPU绑定的代码,例如,等待输入或延迟。Task.Run应仅用于CPU绑定代码blog.stephencleary.com/2013/10/...
geometrikal

The term "await" is literal, so whatever thread you call it on will wait for the result of the method before continuing.这是不正确的-也许您是说Task.Wait()?当您使用时await,它会将方法的其余部分设置为在您等待的内容完成后要执行的延续。它退出您使用它的方法,因此调用者可以继续。然后,当等待的行实际完成时,它将在某个线程(通常是工作线程)上完成该方法的其余部分。
Don Cheadle '18

@geometrikal的核心async/await释放 .NET线程。当您await执行真正异步的操作(例如.NET的File.WriteAsync)时,它会挂起您使用的方法的其余部分await,因此调用方可以继续并可能完成其目的。没有线程阻塞或等待await-ed操作。await完成所需的操作后,该async/await方法的其余部分将放在线程上并执行(类似于回调想法)。
Don Cheadle '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.