1. 异步 async
- 用async 修饰一个方法,表明这个方法是异步的,声明的方法的返回类型必须为 void或Task或 Task<TResult>.
- 方法内部必须含有await 修饰的方法,如果方法内部没有await关键字修饰的表达式,哪怕函数被async修饰也只能算作同步方法,执行的时候也是同步执行的
- 被await修饰的只能是Task或者Task<TResule>类型,通常情况下是一个返回类型Task/Task<Result>的方法,也可以修饰一个Task/Task<TResult>变量,await只能出现在已经用async关键字修饰的异步方法中,下面的代码中就是修饰了一个变量ResultFromTimeConsumingMethod
- 关于被修饰的对象,也就是返回类型是Task和Task<TResult>函数或者Task/Task<TResult>的类型的变量,如果是被修饰对象的前面用await修饰,那么返回值实际是void或者TResult(示例中
ResultFromTimeConsumingMethod
是TimeConsumingMethod()
函数的返回值,也就是Task<string>
类型,当ResultFromTimeConsumingMethod
在前面加了await
关键字后await ResultFromTimeConsumingMethod
实际上完全等于ResultFromTimeConsumingMethod.Result
)。如果没有await
,返回值就是Task
或者Task<TResult>
。
private async Task MethodAsync()
{
var ResultFromTimeConsumingMethod = TimeConsumingMethod();
string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
Console.WriteLine(Result);
//返回值是Task的函数可以不用return
}
//这个函数就是一个耗时函数,可能是IO密集型操作,也可能是cpu密集型工作。
private Task<string> TimeConsumingMethod()
{
var task = Task.Run(()=> {
Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
return "Hello I am TimeConsumingMethod";
});
return task;
}
运行过程:
运行结果:
分析:
1:先调用button1_Click函数,这里是同步执行与一般函数一样,然后遇到了异步函数CalleeAsync。
2:在MethodAsync函数中有await关键字,await的作用是打分裂点。
编译器会把整个函数(MethodAsync)从这里分裂成两个函数。await关键字之前的代码作为一个函数(TimeConsuming函数)await关键字之后的代码作为一个函数(输出Result)。
CalleeChild1在调用方线程执行(在示例中就是主线程Thread1),执行到await关键字之后,另开一个线程耗时工作在Thread3中执行,然后立即返回。这时调用方会继续执行下面的代码CallerChild2(注意是Caller不是Callee)。
在CallerChild2被执行期间,TimeConsumingMethod也在异步执行(可能是在别的线程也可能是CPU不参与操作直接DMA的IO操作)。
当TimeConsumingMethod执行结束后,CalleeChild2也就具备了执行条件,而这个时候CallerChild2可能执行完了也可能没有,由于CallerChild2与CalleeChild2都会在Caller的线程执行,这里就会有冲突应该先执行谁,编译器会在合适的时候在Caller的线程执行这部分代码。示意图如下:
1.1 带有返回值的异步方法
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
var ResultTask = AsyncMethod();
Console.WriteLine(ResultTask.Result);
Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
}
private async Task<string> AsyncMethod()
{
var ResultFromTimeConsumingMethod = TimeConsumingMethod();
string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
Console.WriteLine(Result);
return Result;
}
//这个函数就是一个耗时函数,可能是IO操作,也可能是cpu密集型工作。
private Task<string> TimeConsumingMethod()
{
var task = Task.Run(()=> {
Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
return "Hello I am TimeConsumingMethod";
});
return task;
}
这个程序会出现死锁
1.3 死锁
按照之前我划定的代码块指定,在添加了新代码后CallerChild2与CalleeChild2的划分如上图。
这两部分代码块都是在同一个线程上执行的,也就是主线程Thread1,而且通常情况下CallerChild2是会早于CalleeChild2执行的(毕竟CalleeChild2得在耗时代码块执行之后执行)。
Console.WriteLine(ResultTask.Result);
(CallerChild2)其实是在请求CalleeChild2的执行结果,此时明显CalleeChild2还没有结束没有return任何结果,那Console.WriteLine(ResultTask.Result);
就只能阻塞Thread1等待,直到CalleeChild2有结果。
然而问题就在这,CalleeChild2也是在Thread1上执行的,此时CallerChild2一直占用Thread1等待CalleeChild2的结果,耗时程序结束后轮到CalleeChild2执行的时候CalleeChild2又因Thread1被CallerChild2占用而抢不到线程,永远无法return,那么CallerChild2就会永远等下去,这就造成了死锁。
解决办法有两种一个是把Console.WriteLine(ResultTask.Result);
放到一个新开线程中等待(个人觉得这方法有点麻烦,毕竟要新开线程),还有一个方法是把Caller也做成异步方法:
ResultTask.Result变成了ResultTask 的原因上面也说了,await修饰的Task/Task<TResult>
得到的是TResult。
之所以这样就能解决问题是因为嵌套了两个异步方法,现在的Caller也成了一个异步方法,当Caller执行到await后直接返回了(await拆分方法成两部分),CalleeChild2执行之后才轮到Caller中await后面的代码块(Console.WriteLine(ResultTask.Result);
)。