(给DotNet加星标,提升.Net技能)

转自: Jeffcky  cnblogs.com/CreateMyself/p/5983208.html

前言

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

话题

  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,也就是说将返回值存储到Task中,如果返回值为void那么此时应该会将可能出现的异常存储到上下文中。

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

当将方法用async标识时且返回值为void或者Task或者Task,此时该方法会在当前线程中一直同步执行。

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

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

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

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

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

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

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

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

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

public static async Task CallFuncAsync(T1 t, Func 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));
}

编译后生成如下错误:

0382524c60045c002adeba427cf90d05.png

由上知异步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 > func)
{return await func.Invoke(t);
}

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

string value = await CallFuncAsync(30, GetStringAsync);

此时CallFuncAsync才算是异步运行。

补充

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

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

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

public static async TaskCallFuncAsync(Action action){
return action;
}
public static async TaskGetStringAsync(){
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类里面有这样一个返回类型为 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中查看其方法会有这样的提示:

afaf37a70bb9aec2622868afb7e8950a.png

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

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

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

f8df9bc0125a6571a7ab6e75424f29c4.png

总结 

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

推荐阅读

(点击标题可跳转阅读)

C#8.0 新增功能

多线程下解决资源竞争的7种方法

重新认识 Async/Await 语法糖

看完本文有收获?请转发分享给更多人

关注「DotNet」加星标,提升.Net技能 

285dbba04690ac6fa18d6eff422cec5b.png

好文章,我在看❤️

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐