转发:浅谈async、await关键字 => 深谈async、await关键字

前言

之前写过有关异步的文章,对这方面一直比较弱,感觉还是不太理解,于是会花点时间去好好学习这一块,我们由浅入深,文中若有叙述不稳妥之处,还请批评指正。

话题

(1)是不是将方法用async关键字标识就是异步方法了呢?

(2)是不是没有await关键字的存在async就没有存在的意义了呢?

(3)用异步方法的条件是什么呢,为什么会有这个条件限制?

(4)只能调用.NET Framework内置的用await标识的Task,能否自定义实现呢?

(5)在lambda表达式中是否可以用async和await关键字来实现异步呢(即异步lambda表达式)?

上述抛出这几个话题,明白本文主要讲述的话题以及需要深入了解的知识。

注意:这里我将参照园友【反骨仔】的文章进行进一步解析。

async关键字

例如异步方法是这样的:

        public static async Task<int> asyncMethod()
        {
            return await Task.Run(() => Calculate());
        }

        static int Calculate()
        {
            return 1 + 1;
        }

那要是如下这样写呢?

        public static async Task<int> asyncMethod()
        {
            var task = Task.Run(() => Calculate());
            return task.Result;
        }

那上述这种写法是不是也是异步方法呢?答案是【NO】,既然不是异步方法为什么要用async关键字来进行标识呢?不是很容易被我们所误解呢?好了疑问这么多我们一一来解惑。

当方法用async标识时,编译器主要做了什么呢?

(1)告诉编译器这个方法里面可能会用到await关键字来标识该方法是异步的,如此之后,编译器将会在状态机中编译此方法。接着该方法执行到await关键字时会处于挂起的状态直到该异步动作完成后才恢复继续执行方法后面的动作。

(2)告诉编译器解析出方法的结果到返回类型中,比如说Task或者Task<TResult>,也就是说将返回值存储到Task中,如果返回值为void那么此时应该会将可能出现的异常存储到上下文中。

当方法用async标识时,是不是所有调用者都将是异步的呢?

当将方法用async标识时且返回值为void或者Task或者Task<TReuslt>,此时该方法会在当前线程中一直同步执行。用async标识方法并不会影响方法运行完成是否是同步或者异步,相反,它能够将方法划分成多块,有可能有些在异步中运行,以至于这些方法是异步完成的,而划分异步和同步方法的边界就是使用await关键字。也就是说如果在方法中未用到await关键字时则该方法就是一整块没有所谓的划分,会在同步中运行,在同步中完成。

当方法用async标识时,是否会引起方法的调用会被添加到线程池队列中或者是创建一个新的线程呢?

显然不是这样,当用async标识方法时只是显示告诉编译器在该方法中await关键字可能会被用到,当执行到await关键字开始处于挂起的状态知道异步动作执行完成才恢复(异步操作是在状态机中【有关状态机请看这里:Async和Await异步编程的原理】完成,完成后此时才会创建一个线程),这也就是为什么在方法中方法用async标识如果没有用到await关键字IDE会发出警告的原因。

—————————————————————————————————————————————————————————————————

到了这里我们可以得出结论:无论方法是同步还是异步都可以用async关键字来进行标识,因为用async标识只是显示表明在该方法内可能会用到await关键字使其变为异步方法,而且将该异步方法进行了明确的划分,只有用了await关键字时才是异步操作,其余一并为同步操作。

参数为什么不能使用ref和out关键字

返回类型必须为void或者Task或者Task<TResult>和关键字的标识以及其他就不再叙述,其中有一条是不能使用ref和out关键字,你是背书似的铭记了这一条,还是略加思索了呢?你想过没有为何不可呢?

我们知道用ref和out关键字不过是为了在方法里面改变其值,也就是是当同步完成时我们期望被ref或者out关键字修饰的值会被设置,但是它们可能在异步完成时或者之后才会被设置达不到我们预期,所以在异步方法中不能用ref和out关键字。

lambda表达式是否可以异步呢?

返回类型Task参数可以为lambda表达式或者匿名方法对象,那直接对lambda表达式异步是否可行?下面我们来看看

        public static async Task<T2> CallFuncAsync<T1, T2>(T1 t, Func<T1, T2> func)
        {
            return func.Invoke(t);
        }

        public static async Task<string> GetStringAsync(int value)
        {
            return await Task.Run(() => "xpy0928");
        }

        public static async Task MainAsync()
        {
            string value = await CallFuncAsync<int, string>(30, async (s) => await GetStringAsync(s));
        }

编译后生成如下错误:

由上知异步lambda表达式是不行的,猜测是异步lambda表达式不能转换为表达式树,同时我们看看上述代码,CallFunAsync此时并未是异步方法,上述我们已经叙述过,此时是同步运行,既然上述错误,并且代码也有不可取之处我们接下来一一进行修改。

string value = await CallFuncAsync<int, string>(30, async (s) => await GetStringAsync(s));

修改为:

string value = await CallFuncAsync<int, string>(30, s => GetStringAsync(s).Result);

解决了编译错误,但是未解决CallFuncAsync为异步运行,我们将其修改为异步运行。既然await是针对于Task而操作,我们将CallFuncAsync中的返回参数设置为Task即可。

        public static async Task<T2> CallFuncAsync<T1, T2>(T1 t, Func<T1, Task<T2>> func)
        {
            return await func.Invoke(t);
        }

则最终调用时我们直接调用即可。

   string value = await CallFuncAsync(30, GetStringAsync);

此时CallFuncAsync才算是异步运行。

补充(2016-10-21 23:11)

对于异步表达式有一点其实表述不太正确,其实我一直还是有点怀疑异步lambda表达式真的不行吗,此刻我居然发现这样是可以的:

            var task = Task.Run(async () =>
            {
                await Task.Delay(TimeSpan.FromMilliseconds(5000));
            });

如上不正是异步表达式的影子吗,于是我将上述代码进行了改写,如下:

        public static async Task<Action> CallFuncAsync(Action action)
        {
            return action;
        }

        public static async Task<Action> GetStringAsync()
        {
            return () => Console.WriteLine("xpy0928");
        }

        public static async Task MainAsync()
        {
            await CallFuncAsync(async () => await GetStringAsync());
        }

此时编译通过,说明表述异步表达式并非一定不可以,只是对于无参数的lambda表达式才可以,而对于有参数的lambda表达式如fun则不能执行异步lambda表达式。

至此可以基本下结论:

异步lambda表达式只对于无参数的lambda表达式 才可以(当然这也就没有了什么意义),而对于有参数的lambda表达式则产生编译错误则不能执行异步(猜测是无法转换成对应的表达式树)。(不知是否严谨或者不妥,若有错误之处,还望对此理解的更透彻的园友给出批评性意见)。

为了验证这一点,我们来看看无参数的func委托例子,如下:

        static async Task<string> GetTaskAsync()
        {
            await Task.Delay(TimeSpan.FromMilliseconds(5000));
            return "xpy0928";
        }
var task = Task.Run(async () => await GetTaskAsync());

此时无参数的func委托则编译通过,应该是验证了上述观点(还是有点怀疑我所下的结论)。

await关键字

await关键字是这样用的

await Task.Run(() => "xpy0928");

此时背后究竟发生了什么呢?我们上述也说过异步动作时在状态机中完成,当执行到这里时,编译器会自动生成代码来检测该动作是否已经完成,如果已经完成则继续同步执行await关键字后面的代码,通过判断其状态机状态若未完成则会挂起一个继续的委托为await关键字的对象直到完成为止,调用这个继续动作的委托重新进入未完成的这样一个方法。

比如说: await someObject; 编译器则会生成如下代码:

private class FooAsyncStateMachine : IAsyncStateMachine
{
    // Member fields for preserving “locals” and other necessary state
    int $state;
    TaskAwaiter $awaiter;
    …
    public void MoveNext()
    {
        // Jump table to get back to the right statement upon resumption
        switch (this.$state)
        {
            …
            case 2: goto Label2;
            …
        }
        …
        // Expansion of “await someObject;”
        this.$awaiter = someObject.GetAwaiter();
        if (!this.$awaiter.IsCompleted)
        {
            this.$state = 2;
            this.$awaiter.OnCompleted(MoveNext);
            return;
            Label2:
        }
        this.$awaiter.GetResult();
        …
    }
}

此时讲到这里就要涉及到await背后具体的实现,在Task或者Task<TResult>类里面有这样一个返回类型为 TaskAwaiter 的 GetAwaiter 属性,而TaskAwaiter中有如下属性:

    public struct TaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion
    {

        public bool IsCompleted { get; }
        public void GetResult();
        public void OnCompleted(Action continuation);
        public void UnsafeOnCompleted(Action continuation);
    }

通过IsComplete来判断是否已经完成。这个有什么作用呢?通过看到背后具体实现,我们可以自己简单实现异步扩展方法,当我们在Task中查看其方法会有这样的提示:

下面我们就来实现这样的效果,给TimeSpan添加异步方法:

    public static class Extend
    {
        public static TaskAwaiter GetAwaiter(this TimeSpan timeSpan)
        {

            return Task.Delay(timeSpan).GetAwaiter();
        }
    }

此时异步方法则是这样的:

总结

本节我们详细讲述了async和await关键字的使用和一些基本原理以及解释其原因,希望通过对本文的学习,对大家能够更好的去理解异步,我也在学习中,Over。

原文地址:https://www.cnblogs.com/jiwen/p/9950996.html

时间: 2024-10-10 05:37:36

转发:浅谈async、await关键字 => 深谈async、await关键字的相关文章

浅谈C#中new、override、virtual关键字的区别

OO思想现在已经在软件开发项目中广泛应用,其中最重要的一个特性就是继承,最近偶简单的学习了下在设计模式中涉及到继承这个特性时,所需要用到的关键字,其中有一些关键点,特地整理出来. 一.New 在C#中,new这个关键字使用频率非常高,主要有3个功能: a)   作为运算符用来创建一个对象和调用构造函数. b)   作为修饰符. c)   用于在泛型声明中约束可能用作类型参数的参数的类型. 在本文中,只具体介绍new作为修饰符的作用,在用作修饰符时,new关键字可以在派生类中隐藏基类的方法,也就说

转载 浅谈C/C++中的static和extern关键字

浅谈C/C++中的static和extern关键字 2011-04-21 16:57 海子 博客园 字号:T | T static是C++中常用的修饰符,它被用来控制变量的存贮方式和可见性.extern "C"是使C++能够调用C写作的库文件的一个手段,如果要对编译器提示使用C的方式来处理函数的话,那么就要使用extern "C"来说明.本文主要介绍C/C++中的static和extern关键字. AD: static是C++中常用的修饰符,它被用来控制变量的存贮方

.NET(C#):await返回Task的async方法

一.  FrameWork 4.0之前的线程世界    在.NET FrameWork 4.0之前,如果我们使用线程.一般有以下几种方式: 使用System.Threading.Thread 类,调用实例方法Start()开启一个新线程,调用Abort()方法来提前终止线程. 使用System.Threading.ThreadPool类,调用静态方法QueueUserWorkItem(),将方法放入线程池队列,线程池来控制调用. 使用BeginInvoke,EndInvoke,BeginRead

callback vs async.js vs promise vs async / await

需求: A.依次读取 A|B|C 三个文件,如果有失败,则立即终止. B.同时读取 A|B|C 三个文件,如果有失败,则立即终止. 一.callback 需求A: let read = function (code) { if (code) { return true; } else { return false; } } let readFileA = function (callback) { if (read(1)) { return callback(null, "111");

MVC 如何在一个同步方法(非async)方法中等待async方法

MVC 如何在一个同步方法(非async)方法中等待async方法 问题 首先,在ASP.NET MVC 环境下对async返回的Task执行Wait()会导致线程死锁.例: public ActionResult Asv2() { //dead lock var task = AssignValue2(); task.Wait(); return Content(_container); } private void Assign() { _container = "Hello World&q

ASP.NET MVC 如何在一个同步方法(非async)方法中等待async方法

问题 首先,在ASP.NET MVC 环境下对async返回的Task执行Wait()会导致线程死锁.例: public ActionResult Asv2() { //dead lock var task = AssignValue2(); task.Wait(); return Content(_container); } private void Assign() { _container = "Hello World"; } public async Task AssignVa

浅谈Java泛型中的extends和super关键字(转)

泛型是在Java 1.5中被加入了,这里不讨论泛型的细节问题,这个在Thinking in Java第四版中讲的非常清楚,这里要讲的是super和extends关键字,以及在使用这两个关键字的时候为什么会不同的限制.    首先,我们定义两个类,A和B,并且假设B继承自A.下面的代码中,定义了几个静态泛型方法,这几个例子随便写的,并不是特别完善,我们主要考量编译失败的问题: public class Generic{ //方法一 public static <T extends A> void

浅谈Java泛型中的extends和super关键字

泛型是在Java 1.5中被加入了,这里不讨论泛型的细节问题,这个在Thinking in Java第四版中讲的非常清楚,这里要讲的是super和extends关键字,以及在使用这两个关键字的时候为什么会不同的限制.  首先,我们定义两个类,A和B,并且假设B继承自A. package com.wms.test; import java.util.ArrayList; import java.util.List; public class Generic { public static void

浅谈C/C++中的static和extern关键字 转

原文:http://developer.51cto.com/art/201104/256820.htm static是C++中常用的修饰符,它被用来控制变量的存贮方式和可见性.extern, "C"是使C++能够调用C写作的库文件的一个手段,如果要对编译器提示使用C的方式来处理函数的话,那么就要使用extern "C"来说明. 一.C语言中的static关键字 在C语言中,static可以用来修饰局部变量,全局变量以及函数.在不同的情况下static的作用不尽相同.