此⼊门教程是记录下⽅参考资料视频的过程开发⼯具:Visual Studio 2019
参考资料:
⽬录
异步函数
async和await关键字可以让你写出和同步代码⼀样简洁且结构相同的异步代码
await
1. await关键字简化了附加continuation的过程2. 其结构如下:
var result=await expression; statement(s);
3. 它的作⽤相当于:
var awaiter=expression.GetAwaiter();awaiter.OnCompleted(()=>{
var result=awaiter.GetResult(); statement(s);});
例⼦
static async Task Main(string[] args){
}
//使⽤await的函数⼀定要async修饰//await不能调⽤⽆返回值的函数
static async Task DisplayPrimesCountAsync(){
int result = await GetPrimesCountAsync(2, 1000000); Console.WriteLine(result);}
static Task return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));} async修饰符 1. async修饰符会让编译器把await当作关键字⽽不是标识符(C# 5 以前可能会使⽤await作为标识符)2. async修饰符只能应⽤于⽅法(包括lambda表达式)该⽅法可返回void、Task、Task 3. async修饰符对⽅法的签名或public元数据没有影响(和unsafe⼀样),它只会影响⽅法内部在接⼝内使⽤async是没有意义的 使⽤async来重载⾮async的⽅法却是合法的(只要⽅法签名⼀致)4. 使⽤了async修饰符的⽅法就是“异步函数” 异步⽅法如何执⾏ 1. 遇到await表达式,执⾏(正常情况下)会返回调⽤者就像iterator⾥⾯的yield return 在返回前,运⾏时会附加⼀个continuation到await的task 为了保证task结束时,执⾏会跳回原⽅法,从停⽌的地⽅继续执⾏如果发⽣故障,那么异常就会被重新抛出 如果⼀切正常,那么它的返回值就会赋值给await表达式例⼦ static async Task Main(string[] args){ } //两种⽅法作⽤相同 static void DisplayPrimesCount() { var awaiter = GetPrimesCountAsync(2, 1000000).GetAwaiter(); awaiter.OnCompleted(() => { int result = awaiter.GetResult(); Console.WriteLine(result); });} static async Task DisplayPrimesCountAsync(){ int result = await GetPrimesCountAsync(2, 1000000); Console.WriteLine(result);} static Task return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));} 可以await什么? 1. 你await的表达式通常是⼀个task2. 也可以满⾜下列条件的任意对象: 有GetAwaiter⽅法,它返回⼀个awaiter(实现了INotifyCompletion.OnCompleted接⼝)返回适当类型的GetResult⽅法⼀个bool类型的IsCompleted属性 捕获本地状态 1. await表达式最⽜之处就是它⼏乎可以出现在任何地⽅ 2. 特别的,在异步⽅法内,await表达式可以替换任何表达式,除了lock表达式和unsafe上下⽂例⼦ static async Task Main(string[] args){ } static async void DisplayPrimeCounts(){ for (int i = 0; i < 10; i++) { Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000)); }} static Task return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));} await之后在哪个线程上执⾏ 1. 在await表达式之后,编译器依赖于continuation(通过awaiter模式)来继续执⾏2. 如果在富客户端的UI线程上,同步上下⽂会保证后续是在原线程上执⾏3. 否则,就会在task结束的线程上继续执⾏ UI上的await 1. 例⼦,建议这样写异步函数 public MainWindow(){ InitializeComponent();} async void Go(){ this.Button1.IsEnabled = false; for (int i = 1; i < 5; i++) { this.TextMessage.Text += await this.GetPrimesCountAsync(i * 1000000, 1000000) + \" primes between \" + (i * 1000000) + \" and \" + ((i + 1) * 1000000 - 1) + Environment.NewLine; } this.Button1.IsEnabled = true;} Task { return Task.Run(() => ParallelEnumerable.Range(start, count).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));} private void Button1_Click(object sender, RoutedEventArgs e){ this.TextMessage.Text = null; this.Go();} 2. 本例中,只有GetPeimesCountAsync中的代码在worker线程上运⾏3. Go中的代码会“租⽤”UI线程上的时间 4. 可以说,Go是在消息循环中“伪并发”的执⾏ 也就是说:它和UI线程处理的其它时间是穿插执⾏的 因为这种伪并发,唯⼀能发⽣“抢占”的时刻就是在await期间,这其实简化了线程安全,防⽌重新进⼊即可5. 这种并发发⽣在调⽤栈较浅的地⽅(Task.Run调⽤的代码⾥)6. 为了从该模型获益,真正的并发代码要避免访问共享状态或UI控件例⼦ async void Go(){ this.Button1.IsEnabled = false; string[] urls = \"www.bing.com www.baidu.com www.cnblogs.com\".Split(); int totalLength = 0; try { foreach (string url in urls) { var uri = new Uri(\"http://\" + url); byte[] data = await new WebClient().DownloadDataTaskAsync(uri); this.TextMessage.Text += \"Length of \" + url + \" is \" + data.Length + Environment.NewLine; totalLength += data.Length; } this.TextMessage.Text += \"Total length \" + totalLength; } catch (WebException e) { this.TextMessage.Text += \"Error:\" + e.Message; } finally { this.Button1.IsEnabled = true; }} private void Button1_Click(object sender, RoutedEventArgs e){ this.TextMessage.Text = null; this.Go();} 伪代码: 为本线程设置同步上下⽂(WPF)while(!程序结束){ 等着消息队列中发⽣⼀些事情 发⽣了事情,是哪种消息? 键盘/⿏标消息->触发event handler ⽤户BeginInvoke/Invoke 消息->执⾏委托} 附加到UI元素的event handler通过消息循环执⾏ 因为在UI线程上await,continuation将消息发送到同步上下⽂上,该同步上下⽂通过消息循环执⾏,来保证整个Go⽅法伪并发的在UI线程上执⾏ 与粗粒度的并发相⽐ 1、例如使⽤BackgroundWorker,不推荐这样写异步函数 void Go(){ for (int i = 1; i < 5; i++) { int result = this.GetPrimesCount(i * 1000000, 1000000); this.Dispatcher.BeginInvoke(new Action(() => this.TextMessage.Text += result + \" primes between \" + (i * 1000000) + \" and \" + ((i + 1) * 1000000 - 1) + Environment.NewLine)); } this.Dispatcher.BeginInvoke(new Action(() => this.Button1.IsEnabled = true));} int GetPrimesCount(int start, int count){ return ParallelEnumerable.Range(start, count).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0));} private async void Button1_Click(object sender, RoutedEventArgs e){ this.TextMessage.Text = null; this.Button1.IsEnabled = false; Task.Run(() => this.Go());} 2. 整个同步调⽤图都在worker线程上 3. 必须在代码中到处使⽤Dispatcher.BeginInvoke4. 循环本⾝在worker线程上5. 引⼊了race condition 6. 若实现取消和过程报告,会使得线程安全问题更任意发⽣,在⽅法中新添加任何的代码也是同样的效果 编写异步函数 1. 对于任何异步函数,你可以使⽤Task替代void作为返回类型,让该⽅法成为更有效的异步(可以进⾏await)例⼦ static async Task Main(string[] args){ //不加await关键字就是并⾏,不会等待 await PrintAnswerToLife();} static async Task PrintAnswerToLife(){ await Task.Delay(5000); int answer = 21 * 2; Console.WriteLine(answer);} 2. 并不需要在⽅法体中显式的返回Task。编译器会⽣成⼀个Task(当⽅法完成或发⽣异常时),这使得创建异步的调⽤链⾮常⽅便例⼦ static async Task Main(string[] args){} static async Task Go(){ await PrintAnswerToLife(); Console.WriteLine(\"Done\");} static async Task PrintAnswerToLife(){ await Task.Delay(5000); int answer = 21 * 2; Console.WriteLine(answer);} 3. 编译器会对返回Task的异步函数进⾏扩展,使其成为当发送信号或发⽣故障时使⽤TaskCompletionSource来创建Task的代码⼤致代码 static Task PrintAnswerToLide(){ var tcs = new TaskCompletionSource