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<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修饰符
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<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什么?
1. 你await的表达式通常是⼀个task
2. 也可以满⾜下列条件的任意对象:
有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<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之后在哪个线程上执⾏
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<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();
}
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 www.baidu wwwblogs".Split();
int totalLength = 0;
try
{
foreach (string url in urls)
{
var uri = new Uri("" + 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
writeline函数{
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.BeginInvoke
4. 循环本⾝在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<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;
}
4. 因此,当返回Task的异步⽅法结束的时候,执⾏就会跳回到对它进⾏await的地⽅(通过continuation)
编写异步函数,富客户端场景下
1. 富客户端场景下,执⾏在此刻会跳回到UI线程(如果⽬前不在UI线程的话)
2. 否则,就在continuation返回的任意线程上继续执⾏
3. 这意味着,在异步调⽤图中向上冒泡的时候,不会发⽣延迟成本,除⾮是UI线程启动的第⼀次“反弹”
返回Task
1. 如果⽅法体返回TResult,那么异步⽅法就可以返回Task
例⼦
static async Task Main(string[] args)
{
}
static async Task<int> 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<int> 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);
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论