【译】ASP.NET 中关于 Async 和 Await 的概述

原文地址 : 【A Simple Explanation of Async and Await in ASP.NET】

同步和异步.png

有时,我在日常工作中的技术分享迫使我去学习一些新的东西,有些甚至是以前从未接触过的。上周就出现了这种情况,我的同事投票赞成下一次的技术分享主题为.NET 中的 Async 和 Await 异步编程。这两个关键字使得异步编程在 .NET 4.5 之后变得更简单了。

老实说,直到上一周,对于异步编程我知之甚少。但在我做了一些研究并创建了我自己的简单项目之后,我开始理解了为什么 Stephen Cleary 会说 :“异步编程将从根本上改变大多数代码的编写方式”。

本着不关心我的结巴,只要我在学习(参见),我决定直接学习 async , await 和 异步编程。 但是我遇到一个问题:我发现很少有资源能够用简单的术语向我解释异步编程的概念。

我试图用这篇文章来弥补这个空缺。

我并不打算去详细介绍 .NET 中关于异步编程的技术实现细节,相反,我会专注于将这些不透明的概念分解成简单的概念,使得它更好的被理解。这帮助我更好的理解当我使用这些关键字时我正在做什么,同时,希望它也能帮助到你。开始吧!

什么是异步编程?

异步编程是在编写代码,这些代码允许在没有“阻塞”的情况下同时发生几件事情,或者等待其它的事情完成。它不同于同步编程,在同步编程中,所有的事情都按照它所写的顺序发生(如果你为一个活人编写代码,那么可能是同步代码)。

让我们来看一个 C# 中同步编程的方法:

public string GetNameAndContent()
{
    var name = GetLongRunningName(); //调用另一个webservice,需要 1 分钟。
    var content = GetContent();  //需要 30 秒
    return name + ": " + content;
}

每一次调用这个方法,调用者必须等待一分钟才能恢复处理。这一分钟的时间被浪费了,它可以用来做其他的事情。

而 .NET 的异步编程,我们可以像这样改变这个方法:

public async Task<string> GetNameAndContent()
{
    var nameTask = GetLongRunningName(); //这个方法是异步的
    var content = GetContent(); //这个方法是同步的
    var name = await nameTask;
    return name + ": " + content;
}

我们改变了这个方法的三个地方:

  • 1.我们把方法改为了异步 async。它会告诉编译器这个方法可以异步执行。
  • 2.我们使用 await 关键字修饰 nameTask 变量,它告诉编译器我们最终需要GetLongRunningName()方法的结果,但是我们不需要阻塞这个调用。
  • 3.我们将方法的返回类型改为Task<string>。它通知调用者返回的最终类型为字符串,但不是立即获得,我们不可以做任何其他的事情直到GetLongRunningName()方法调用结束。

但是即使是这样,它任然很模糊。当 “等待” GetLongRunningName()完成时,我们实际在做什么?

这很难用简单的术语来表达,但我还是会尝试的。本质上,系统希望执行GetLongRunningName(),因为它首先被调用,但它是一个异步任务,所以我们需要等待它,控制器被抛出去执行GetContent(),这意味着我们现在有两种方法在同时运行。这不是转移到另一个线程,使用 asyncawait不会导致线程的创建。

(如果您想要更深入地解释在异步调用过程中发生了什么,参见MSDN。这是我能找到的最好的例子,有一个图。)

等待多个调用

让我来看另一个简单是例子。有一个叫 ContentManagement 的类,它既包含了同步方法也包含了异步方法:

public class ContentManagement
{
    public string GetContent()
    {
        Thread.Sleep(2000);
        return "content";
    }

    public int GetCount()
    {
        Thread.Sleep(5000);
        return 4;
    }

    public string GetName()
    {
        Thread.Sleep(3000);
        return "Matthew";
    }
    public async Task<string> GetContentAsync()
    {
        await Task.Delay(2000);
        return "content";
    }

    public async Task<int> GetCountAsync()
    {
        await Task.Delay(5000);
        return 4;
    }

    public async Task<string> GetNameAsync()
    {
        await Task.Delay(3000);
        return "Matthew";
    }
}

ContentManagement 是一个简单的类,它模拟了一些需要长时间运行的执行方法。注意,其中有三个方法被标注了 async,并且(通过约定)将Async这个关键字添加到方法名中。我们将在下面的潜在问题部分中解释为什么我们需要同步和异步方法。

现在,然给我们写一个如下的 MVC 控制器:

public class HomeController : Controller
{
    [HttpGet]
    public ActionResult Index()
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        ContentManagement service = new ContentManagement();
        var content = service.GetContent();
        var count = service.GetCount();
        var name = service.GetName();

        watch.Stop();
        ViewBag.WatchMilliseconds = watch.ElapsedMilliseconds;
        return View();
    }

    [HttpGet]
    public async Task<ActionResult> IndexAsync()
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        ContentManagement service = new ContentManagement();
        var contentTask = service.GetContentAsync();
        var countTask = service.GetCountAsync();
        var nameTask = service.GetNameAsync();

        var content = await contentTask;
        var count = await countTask;
        var name = await nameTask;
        watch.Stop();
        ViewBag.WatchMilliseconds = watch.ElapsedMilliseconds;
        return View("Index");
    }
}

(顺便说一下,Stopwatch类在尝试记录执行时间时非常有用。)

注意这两个动作的作用。在这两种情况下,操作都调用 ContentManagement 服务中的方法,但在其中一个操作中,它们是异步执行的。

在我们的 Index 视图中,我们有一个显示 WatchMilliseconds 值的输出。让我们来看看Index中结果是什么:

直观地说,这是有道理的。我们称为三种方法;他们分别花了 2 秒,5 秒和 3 秒来执行;所以总执行时间应该是 2 + 5 + 3 = 10 秒。

现在看看如果我们调用 IndexAsync 操作会发生什么:

我们从哪里能得到这个数字? 这三个任务中的最长任务需要的时间为5秒。 通过设计这个来使用async,我们能减少总花费时长的一半!通过编写一些额外的代码就可以获得很好的加速效果!

返回类型

如你所见,我们为什么要编写异步编程代码,现在看来至少对我来说是有意义的。但是,到底我们在 IndexAsync 操作中使用的Task<ActionResult>是什么东西?

含有async关键字的方法中有三种返回类型可以选择:

  • Task:这个类表示一个异步操作,并且可以被等待;
  • Task<T>:这个类表示一个有返回值的异步操作,并且可以被等待;
  • void:如果一个异步方法返回void,它不能被等待。这实际上把方法变成了“fire and forget(阅后即焚)”方法,这样的情况很少出现。进一步,返回void的异步方法的错误处理有点不同,比如shown by Stephen Cleary没有理由使用void作为异步调用的返回类型,除非完全不关心调用是否实际完成。

简而言之,几乎所有的异步方法都会使用 Task 或 Task<T> 作为它们的返回类型。Task 类表示异步操作本身,而不是action的结果。在一个 Task 中调用 await 意味着我们需要等待这个 Task 执行完成,而在 Task<T> 的情况下,需要检索任务返回的值。

潜在问题

让我们注意一些事情:大多数应用程序在实现异步编程时可能不会看到这样的戏剧性的改进(比如50%加速!),同样,我们不会对单个的方法进行压力测试,只是同时执行它们。事实上,如果我们不正确地设计我们的异步方法,实际上可能会损害整体性能。

当我们将一个方法标记为 async 时,编译器在后台生成一个状态机;这是额外的代码。如果我们编写好的、稳定的异步代码,创建这种额外结构所需的时间不会对我们造成任何影响,因为运行异步的好处超过了构建状态机的成本。然而,如果我们的 async 方法没有 await,方法将会被同步运行,我们将花费额外的时间来创建我们没有使用的状态机。

还有一个潜在的问题需要注意。我们不能从同步方法调用异步方法。因为在所有情况下, asyncawait应该是一起的,我们需要在所有的方法上都有异步。这就是为什么我们在前面的 ContentManagement 类中需要单独的 async 方法。最终,这导致了更多的代码,这意味着更多的东西理论上可以中断。然而,由于对我们想要完成的事情有了良好的设计和坚实的理解,拥有额外的代码也会带来更大的性能,所以在我看来,这是一个公平的交易。

摘要

在实现异步的过程中付出一点额外的努力,对于提高我们的应用程序的性能和响应能力有很长的一段路要走。.NET使 asyncawait关键字变得容易,我们可以简单、简洁地、快速地实现异步设计。

我在这个练习之前完全不懂异步编程;现在我至少可以说我不是一无所知。希望你也一样!

如果你想学更多的内容,可以看一下 Alex Davies 写的 《Async in C# 5.0》 ,我在试图理解MVC web应用程序中异步/等待的情况时,我读过,它帮了我很多。

我使用 Visual Studio 2015 和 ASP.NET MVC(包括我们之前使用的ContentManagement类和HomeController) 创建了一个简单项目。我把它放到了Github,你可以check out下来进行练习。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容