您的当前位置:首页C#异步编程基础(八)异步函数

C#异步编程基础(八)异步函数

2021-06-04 来源:爱问旅游网
C#异步编程基础(⼋)异步函数

此⼊门教程是记录下⽅参考资料视频的过程开发⼯具: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 GetPrimesCountAsync(int start, int count){

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 GetPrimesCountAsync(int start, int count){

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 GetPrimesCountAsync(int start, int count){

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 GetPrimesCountAsync(int start, int count)

{

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(); var awaiter = Task.Delay(5000).GetAwaiter(); awaiter.OnCompleted(() => {

try {

awaiter.GetResult(); int answer = 21 * 2;

Console.WriteLine(answer); tcs.SetResult(null); }

catch (Exception e) {

tcs.SetException(e); } });

return tcs.Task;}

4. 因此,当返回Task的异步⽅法结束的时候,执⾏就会跳回到对它进⾏await的地⽅(通过continuation)

编写异步函数,富客户端场景下

1. 富客户端场景下,执⾏在此刻会跳回到UI线程(如果⽬前不在UI线程的话)2. 否则,就在continuation返回的任意线程上继续执⾏

3. 这意味着,在异步调⽤图中向上冒泡的时候,不会发⽣延迟成本,除⾮是UI线程启动的第⼀次“反弹”

返回Task

1. 如果⽅法体返回TResult,那么异步⽅法就可以返回Task例⼦

static async Task Main(string[] args){

}

static async Task GetAnswerToLiife(){

await Task.Delay(5000); int answer = 21 * 2; return answer;}

2. 其原理就是给TaskCompletion发送的信号带有值,⽽不是null例⼦

static async Task PrintAnswerToLife(){

int answer = await GetAnswerToLife(); Console.WriteLine(answer);}

static async Task GetAnswerToLife(){

await Task.Delay(5000); int answer = 21 * 2; return answer;}

3. 与同步编程很相似,是故意这样设计的同步版本

static void Main(string[] args){

}

static void Go(){

PrintAnswerToLife();

Console.WriteLine(\"Done\");}

static void PrintAnswerToLife(){

int answer = GetAnswerToLife(); Console.WriteLine(answer);}

static int GetAnswerToLife(){

Thread.Sleep(5000); int answer = 21 * 2; return answer;}

C#中如何设计异步函数

1. 以同步的⽅式编写⽅法

2. 使⽤异步调⽤来替代同步调⽤,并且进⾏await

3. 除了顶层⽅法外(UI控件的event handler,因为没有await调⽤),把你⽅法的返回类型升级为Task或Task,这样它们就可以进⾏await了

编译器能对异步函数⽣成Task意味着什么?

1. ⼤多数情况下,你只需要在初始化IO-Bound并发的底层⽅法⾥显式的初始化TaskCompletionSource,这种情况很少见2. 针对初始化Compute-Bound的并发⽅法,你可以使⽤Task.Run来创建Task

异步调⽤图执⾏

例⼦

static async Task Main(string[] args){

//Main Thread await Go();}

static async Task Go(){

var task = PrintAnswerToLife(); await task;

Console.WriteLine(\"Done\");}

static async Task PrintAnswerToLife(){

var task = GetAnswerToLife(); int answer = await task; Console.WriteLine(answer);}

static async Task GetAnswerToLife(){

var task = Task.Delay(5000); await task;

int answer = 21 * 2; return answer;}

1. 整个执⾏与之前同步例⼦中调⽤图的执⾏顺序是⼀样的,因为我们对每个异步函数的调⽤都进⾏了await2. 在调⽤图中创建了⼀个没有并⾏和重叠的连续流

3. 每个await在执⾏中都创建了⼀个间隙,在间隙后,程序可以从中断处恢复执

并⾏(Parallelism)

1. 不使⽤await来调⽤异步函数会导致并⾏执⾏的发⽣2. 例如:_button.Click+=(sender,args)=>Go();

主线程仍然在执⾏,GO()也在执⾏确实也能满⾜保持UI响应的并发要求3. 同样,可以并⾏跑两个操作:

var task1=PrintAnswerToLife(); var task2=PrintAnswerToLife(); await task1; await task2;

异步Lambda表达式

1. 匿名⽅法(包括Lambda表达式),通过使⽤async也可以变成异步⽅法2. 调⽤⽅式也⼀样

static async Task Main(string[] args){

Func unnamed = async () => {

await Task.Delay(1000); Console.WriteLine(\"Foo\"); };

await NamedMethod(); await unnamed();}

static async Task NamedMethod(){

await Task.Delay(1000); Console.WriteLine(\"Foo\");}

3. 附加event handler的时候也可以使⽤异步Lambda表达式例⼦

myButton.Click+=async (sender,args)=>{

await Task.Delay(1000); myButton.Content=\"Done\";}

相当于

myButton.Click+=ButtonHandler;

async void ButtonHandler(object sender,EventArgs args){

await Task.Delay(1000); myButton.Content=\"Done\";}

4. 也可以返回Task

static async Task Main(string[] args){

Func> unnamed = async () => {

await Task.Delay(1000); return 123; };

int answer = await unnamed();}

异步函数 结束

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- 版权所有