C# 异步编程 async、await
C#5.0的时候引入了async和await两个修饰符,成为异步编程的核心关键字。
async 是修饰符,表明方法含有异步操作,但并不是说整个方法是异步的。async修饰的方法会先同步执行到第一处await的地方而后开始异步。
await可以理解为一种异步特有的“return”。即返回一个正在运行的异步过程。并且等待该异步过程结束后再继续向await的下一句运行。
例如下方法
private static void Main(string[] args)
{
Console.WriteLine("Application Start");
AsyncTask1();
Console.WriteLine("Application End");
}
private static async Task AsyncTask1()
{
Console.WriteLine("AsyncVoid1");
Thread.Sleep(1000);
Console.WriteLine("AsyncVoid1: befor await");
await Task.Run(() =>
{
Console.WriteLine("AsyncVoid1: Task Runing");
Thread.Sleep(2000);
});
Console.WriteLine("AsyncVoid1: after await");
}
运行输出如下:
Application Start
AsyncVoid1
AsyncVoid1: befor await
AsyncVoid1: Task Runing
Application End
AsyncVoid1: after await
异步返回类型
async是方法的修饰,其对应的方法的返回值有
- void
- Task
- Task<TResult>、
- 任何具有可访问的 GetAwaiter 方法的类型(GetAwaiter 方法返回的对象必须实现 ystem.Runtime.CompilerServices.ICriticalNotifyCompletion 接口)。
- IAsyncEnumerable<T>(对于返回异步流的异步方法)。
常用的是前三个。暂且只整理前三者的区别与不同。
async void
async不建议修饰void方法,因为void 方法无法使用await。比如上面的例子中,AsyncTask1方法再调用时可以不加await,也可以加await来实现在调用程序结束前等待其执行结束。
比如调用方式改成如下:
static void Main(string[] args)
{
//控制台Main入口不能加async
MainAsync();
}
//方法内有await时,方法必须被async修饰。
private static async void MainAsync()
{
Console.WriteLine("Application Start");
await AsyncTask1();
Console.WriteLine("Application End");
}
运行结果如下:
Application Start
AsyncVoid1
AsyncVoid1: befor await
AsyncVoid1: Task Runing
AsyncVoid1: after await
Application End
如果将AsyncTask1的返回类型变成void,则调用方中就不能用await来强制同步了。
private static async void MainAsync()
{
Console.WriteLine("Application Start");
await AsyncTask1();//编译报错,“无法等待void”
Console.WriteLine("Application End");
}
private static async void AsyncTask1()
{
Console.WriteLine("AsyncVoid1");
Thread.Sleep(1000);
Console.WriteLine("AsyncVoid1: befor await");
await Task.Run(() =>
{
Console.WriteLine("AsyncVoid1: Task Runing");
Thread.Sleep(2000);
});
Console.WriteLine("AsyncVoid1: after await");
}
async可以修饰返回void的方法,是因为这种多用于事件,例如按钮点击事件等,这些情景就非常符合async void的使用场景。
public class NaiveButton
{
public event EventHandler? Clicked;
public void Click()
{
Console.WriteLine("Somebody has clicked a button. Let's raise the event...");
Clicked?.Invoke(this, EventArgs.Empty);
Console.WriteLine("All listeners are notified.");
}
}
public class AsyncVoidExample
{
public static Main()
{
var button = new NaiveButton();
button.Clicked += OnButtonClickedAsync;
Console.WriteLine("Before button.Click() is called...");
button.Click();
Console.WriteLine("After button.Click() is called...");
}
private static async void OnButtonClickedAsync(object? sender, EventArgs e)
{
Console.WriteLine(" Handler 2 is starting...");
Task.Delay(100).Wait();
Console.WriteLine(" Handler 2 is about to go async...");
await Task.Delay(500);
Console.WriteLine(" Handler 2 is done.");
}
}
async Task
返回Task类型的。个人概括的说,就是“执行操作但不返回任何值的异步方法”。即await 等待的一个无返回值的Task。
例如前文已有例子:
private static async Task AsyncTask1()
{
Console.WriteLine("AsyncVoid1");
Thread.Sleep(1000);
Console.WriteLine("AsyncVoid1: befor await");
await Task.Run(() =>
{
Console.WriteLine("AsyncVoid1: Task Runing");
Thread.Sleep(2000);
});
Console.WriteLine("AsyncVoid1: after await");
}
需要说明的是,这里说的是async修饰的Task返回类型,从外部调用上,她只需要返回Task类型的实例对象。但实际上行需要返回已经开始运行Task的对象。在调用处不能通过Task.Start()来开始任务,await后接一个未开始的任务也只能如等一个已死的人回心转意。
从设计上讲,async和await总是搭配使用的,await后接的必定是正在运行的Task。
async Task<TResult>
返回类型为Task<TResult>的异步方法,和返回类型为Task<TResult>的方法是两回事。直接看代码:
private static async Task<int> AsyncFuncReturn1()
{
await Task.Run(() =>
{
Thread.Sleep(500);
});
return 1;
}
private static Task<int> AsyncFuncReturn2()
{
return Task<int>.Run(() =>
{
Thread.Sleep(500);
return 1;
});
}
后者只是将Task的实例化过程进行包装。前者具有两方面功能,一方提供Task实例的操控权力,另一方面说明异步方法的返回值类型。两者是独立的。即异步方法中的Task类型可以自定义,只需要确保方法的返回值类型正确即可。
但是,这两个方法再调用时却是一致的
private async static void Caller()
{
//通过await 等待获取结果
int s1 = await AsyncFuncReturn1();
int s2 = await AsyncFuncReturn2();
//通过Result(转同步)获取结果
int s11 = AsyncFuncReturn1().Result;
int s12 = AsyncFuncReturn2().Result;
//先获取Task
var t1 = AsyncFuncReturn1();
var t2 = AsyncFuncReturn2();
//在Task运行时先同步处理其他事情之后再获取结果
int s111 = t1.Result;
int s112 = t2.Result;
}
但优先考虑使用有async修饰的方式,因为这样,如果你没有写await,编译器通常会提醒你,以防止你返回的是一个没有开始的任务。
await 和 Task.Wait()/Task.Result
await 和Task.Wait()的作用都有等待任务结束的功能,不同的是,就像前面说的,await还有一层“return”的意思,或者说更像“yield”。即让await所在方法的被调用者可以继续下一步操作。(这里只是说“可以”,即对外产生这样的能力,而如果await所在方法返回的值是void类型,调用者就不能强制同步了)
依然用上面的示例:
private static void Main(string[] args)
{
Console.WriteLine("Application Start");
AsyncTask1();
Console.WriteLine("Application End");
}
private static async Task AsyncTask1()
{
Console.WriteLine("AsyncVoid1");
Thread.Sleep(1000);
Console.WriteLine("AsyncVoid1: befor await");
await Task.Run(() =>
{
Console.WriteLine("AsyncVoid1: Task Runing");
Thread.Sleep(2000);
});
Console.WriteLine("AsyncVoid1: after await");
}
运行输出如下:
Application Start
AsyncVoid1
AsyncVoid1: befor await
AsyncVoid1: Task Runing
Application End
AsyncVoid1: after await
可以看到,先输出了Application End,即AsyncTask1中的await等待异步任务时,主程序依然在运行。
一方面,如果将AsyncTask1改造成如下
private static void AsyncTask1_2()
{//没有await,不需要async修饰,也不会返回Task.
Console.WriteLine("AsyncVoid1");
Thread.Sleep(1000);
Console.WriteLine("AsyncVoid1: befor await");
Task.Run(() =>
{
Console.WriteLine("AsyncVoid1: Task Runing");
Thread.Sleep(2000);
}).Wait();
Console.WriteLine("AsyncVoid1: after await");
}
这样调用AsyncTask1_2()方法将变成纯粹的同步任务。
另一方面,如果不改变AsyncTask1()方法,而改变调用方式:
private static void Main(string[] args)
{
Console.WriteLine("Application Start");
AsyncTask1().Wait();
Console.WriteLine("Application End");
}
其也是将AsyncTask1()强制转换同步方法。
Task.Wait()和Task.Result在异步同步问题上是一致的,不同的是Result是针对Task<TResult>任务的,先内部Wait()而后返回结果。
async与await的执行顺序
需要引用一下巨硬家的官方讲解,还是很清楚的
关系图中的数字对应于以下步骤,在调用方法调用异步方法时启动。
- 调用方法调用并等待 GetUrlContentLengthAsync 异步方法。
- GetUrlContentLengthAsync 可创建 HttpClient 实例并调用 GetStringAsync 异步方法以下载网站内容作为字符串。
- GetStringAsync 中发生了某种情况,该情况挂起了它的进程。 可能必须等待网站下载或一些其他阻止活动。 为避免阻止资源,GetStringAsync 会将控制权出让给其调用方 GetUrlContentLengthAsync。
- 由于尚未等待 getStringTask,因此,GetUrlContentLengthAsync 可以继续执行不依赖于 GetStringAsync 得出的最终结果的其他工作。 该任务由对同步方法 DoIndependentWork 的调用表示。
- DoIndependentWork 是完成其工作并返回其调用方的同步方法。
- GetUrlContentLengthAsync 已运行完毕,可以不受 getStringTask 的结果影响。 接下来,GetUrlContentLengthAsync 需要计算并返回已下载的字符串的长度,但该方法只有在获得字符串的情况下才能计算该值。
- GetStringAsync 完成并生成一个字符串结果。 字符串结果不是通过按你预期的方式调用 GetStringAsync 所返回的。 (记住,该方法已返回步骤 3 中的一个任务)。相反,字符串结果存储在表示 getStringTask 方法完成的任务中。 await 运算符从 getStringTask 中检索结果。 赋值语句将检索到的结果赋给 contents。
- 当 GetUrlContentLengthAsync 具有字符串结果时,该方法可以计算字符串长度。 然后,GetUrlContentLengthAsync 工作也将完成,并且等待事件处理程序可继续使用。 在此主题结尾处的完整示例中,可确认事件处理程序检索并打印长度结果的值。 如果你不熟悉异步编程,请花 1 分钟时间考虑同步行为和异步行为之间的差异。 当其工作完成时(第 5 步)会返回一个同步方法,但当其工作挂起时(第 3 步和第 6 步),异步方法会返回一个任务值。 在异步方法最终完成其工作时,任务会标记为已完成,而结果(如果有)将存储在任务中。
简单说下重点,就是await执行完后会回到方法调用方地方。
private static void Main(string[] args)
{
Caller();
}
private static void Caller()
{
Console.WriteLine("Caller:Call AsyncFunc");
AsyncFunc();
Console.WriteLine("Caller:Call Finished");
Thread.Sleep(300);
Console.WriteLine("Caller:sleep 300ms");
Thread.Sleep(300);
Console.WriteLine("Caller:sleep 300ms");
Console.WriteLine("Caller:end");
}
private static async Task AsyncFunc()
{
Console.WriteLine("AsyncFunc:befor await");
await Task.Run(() => { Console.WriteLine("await task running"); Thread.Sleep(500); });
Console.WriteLine("AsyncFunc:after await");
}
Caller:Call AsyncFunc
AsyncFunc:befor await
Caller:Call Finished
await task running
Caller:sleep 300ms
AsyncFunc:after await
Caller:sleep 300ms
Caller:end
即是说上面例子中的AsyncFunc()异步方法执行到await会返回到Caller()中,如果Caller()中对异步方法的调用方式是同步的(如上)则Caller自身线程继续执行后续任务。
这一点在界面化编程中较为重要,代码不正确顺序会在窗体程序中造成死锁或异常,这一点在后续文章《C# 异步编程 WinForm中的常见问题》中做详细说明。
更多推荐
所有评论(0)