C# 异步编程基础(八) 异步函数
此入门教程是记录下方参考资料视频的过程
开发工具:Visual Studio 2019
目录
C# 异步编程基础(六)Continuation 继续/延续 、TaskCompletionSource、实现 Task.Delay
C# 异步编程基础(十) 取消(cancellation)、进度报告、TAP(Task-Based Asynchronous Pattern)、Task组合器
异步函数
async和await关键字可以让你写出和同步代码一样简洁且结构相同的异步代码
await
- await关键字简化了附加continuation的过程
- 其结构如下:
var result=await expression;
statement(s);
- 它的作用相当于:
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<int> 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修饰符
- async修饰符会让编译器把await当作关键字而不是标识符(C# 5 以前可能会使用await作为标识符)
- async修饰符只能应用于方法(包括lambda表达式)
该方法可返回void、Task、Task - async修饰符对方法的签名或public元数据没有影响(和unsafe一样),它只会影响方法内部
在接口内使用async是没有意义的
使用async来重载非async的方法却是合法的(只要方法签名一致) - 使用了async修饰符的方法就是“异步函数”
- 假设有一个async函数Foo,如果不使用await调用Foo,在没有Task.Run的情况下是顺序执行,但是如果Foo中有await调用其它async函数,那么就会先继续执行Foo之后的代码,这就是非阻塞
异步方法如何执行
- 遇到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<int> 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什么?
- 你await的表达式通常是一个task
- 也可以满足下列条件的任意对象:
有GetAwaiter方法,它返回一个awaiter(实现了INotifyCompletion.OnCompleted接口)
返回适当类型的GetResult方法
一个bool类型的IsCompleted属性
捕获本地状态
- await表达式最牛之处就是它几乎可以出现在任何地方
- 特别的,在异步方法内,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<int> 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之后在哪个线程上执行
- 在await表达式之后,编译器依赖于continuation(通过awaiter模式)来继续执行
- 如果在富客户端的UI线程上,同步上下文会保证后续是在原线程上执行
- 否则,就会在task结束的线程上继续执行
UI上的await
- 例子,建议这样写异步函数
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<int> 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();
}
- 本例中,只有GetPeimesCountAsync中的代码在worker线程上运行
- Go中的代码会“租用”UI线程上的时间
- 可以说,Go是在消息循环中“伪并发”的执行
也就是说:它和UI线程处理的其它时间是穿插执行的
因为这种伪并发,唯一能发生“抢占”的时刻就是在await期间,这其实简化了线程安全,防止重新进入即可 - 这种并发发生在调用栈较浅的地方(Task.Run调用的代码里)
- 为了从该模型获益,真正的并发代码要避免访问共享状态或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());
}
- 整个同步调用图都在worker线程上
- 必须在代码中到处使用Dispatcher.BeginInvoke
- 循环本身在worker线程上
- 引入了race condition
- 若实现取消和过程报告,会使得线程安全问题更任意发生,在方法中新添加任何的代码也是同样的效果
编写异步函数
- 对于任何异步函数,你可以使用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);
}
- 并不需要在方法体中显式的返回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);
}
- 编译器会对返回Task的异步函数进行扩展,使其成为当发送信号或发生故障时使用TaskCompletionSource来创建Task的代码
大致代码
static Task PrintAnswerToLide()
{
var tcs = new TaskCompletionSource<object>();
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;
}
- 因此,当返回Task的异步方法结束的时候,执行就会跳回到对它进行await的地方(通过continuation)
编写异步函数,富客户端场景下
- 富客户端场景下,执行在此刻会跳回到UI线程(如果目前不在UI线程的话)
- 否则,就在continuation返回的任意线程上继续执行
- 这意味着,在异步调用图中向上冒泡的时候,不会发生延迟成本,除非是UI线程启动的第一次“反弹”
返回Task
- 如果方法体返回TResult,那么异步方法就可以返回Task
例子
static async Task Main(string[] args)
{
}
static async Task<int> GetAnswerToLiife()
{
await Task.Delay(5000);
int answer = 21 * 2;
return answer;
}
- 其原理就是给TaskCompletion发送的信号带有值,而不是null
例子
static async Task PrintAnswerToLife()
{
int answer = await GetAnswerToLife();
Console.WriteLine(answer);
}
static async Task<int> GetAnswerToLife()
{
await Task.Delay(5000);
int answer = 21 * 2;
return answer;
}
- 与同步编程很相似,是故意这样设计的
同步版本
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#中如何设计异步函数
- 以同步的方式编写方法
- 使用异步调用来替代同步调用,并且进行await
- 除了顶层方法外(UI控件的event handler,因为没有await调用),把你方法的返回类型升级为Task或Task
,这样它们就可以进行await了
编译器能对异步函数生成Task意味着什么?
- 大多数情况下,你只需要在初始化IO-Bound并发的底层方法里显式的初始化TaskCompletionSource,这种情况很少见
- 针对初始化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<int> GetAnswerToLife()
{
var task = Task.Delay(5000);
await task;
int answer = 21 * 2;
return answer;
}
- 整个执行与之前同步例子中调用图的执行顺序是一样的,因为我们对每个异步函数的调用都进行了await
- 在调用图中创建了一个没有并行和重叠的连续流
- 每个await在执行中都创建了一个间隙,在间隙后,程序可以从中断处恢复执
并行(Parallelism)
- 不使用await来调用异步函数会导致并行执行的发生
- 例如:
_button.Click+=(sender,args)=>Go();
主线程仍然在执行,GO()也在执行
确实也能满足保持UI响应的并发要求 - 同样,可以并行跑两个操作:
var task1=PrintAnswerToLife();
var task2=PrintAnswerToLife();
await task1;
await task2;
异步Lambda表达式
- 匿名方法(包括Lambda表达式),通过使用async也可以变成异步方法
- 调用方式也一样
static async Task Main(string[] args)
{
Func<Task> unnamed = async () =>
{
await Task.Delay(1000);
Console.WriteLine("Foo");
};
await NamedMethod();
await unnamed();
}
static async Task NamedMethod()
{
await Task.Delay(1000);
Console.WriteLine("Foo");
}
- 附加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";
}
- 也可以返回Task
static async Task Main(string[] args)
{
Func<Task<int>> unnamed = async () =>
{
await Task.Delay(1000);
return 123;
};
int answer = await unnamed();
}