C# 异步编程基础(七)异步原理
此入门教程是记录下方参考资料视频的过程
开发工具:Visual Studio 2019
目录
C# 异步编程基础(六)Continuation 继续/延续 、TaskCompletionSource、实现 Task.Delay
C# 异步编程基础(十) 取消(cancellation)、进度报告、TAP(Task-Based Asynchronous Pattern)、Task组合器
同步和异步
- 同步操作会在返回调用者之前完成它的工作
- 异步操作会在返回调用者之后去做它的(大部分)工作
异步发方法更为少见,会启用并发,因为它的工作会与调用者并行执行
异步的方法通常很快(立即)就返回到调用者,所以叫非阻塞方法 - 目前见到的大部分的异步方法都是通用目的的:
Thread.Start
Task.Run
可以将Continuation附加到Task的方法
...
什么是异步编程
- 异步编程的原则是将长时间运行的函数写成异步的
- 传统的做法是将长时间运行的函数写出同步的,然后从新的线程或Task进行调用,从而按需引入并发
- 上述异步方式的不同之处在于,它是从长时间运行函数的内部启动并发。这有两点好处:
IO-Bound并发可不使用线程来实现(用回调)。可提高可扩展和执行效率
富客户端在worker线程会使用更少的代码,简化了线程安全性
异步编程的两种用途
- 编写高效处理大量并发IO的应用程序(典型的:服务端应用)
挑战的并不是线程安全(因为共享状态通常是最小化的),而是执行效率
特别的,每个网络请求并不会消耗一个线程 - 在富客户端应用里简化线程安全
如果call graph(调用图)中任何一个操作是长时间运行的,那么整个call graph必须运行在worker线程上,以保证UI响应
得到一个横跨多个方法的单一并发操作(粗粒度)
需要为call graph中的每个方法考虑线程安全
异步的call graph,直到需要的时候才开启一个线程,通常较浅(IO-Bound操作完全不需要)
其它的方法可以在UI线程执行,线程安全简化
并发的粒度适中
一连串小的并发操作,操作之间会弹回到UI线程
经验之谈
- 为了获得上述好处,下列操作建议异步编写:
IO-Bound和Complute-Bound操作
执行超过50毫秒的操作 - 另一方面过细的粒度会损害性能,因为异步操作也有开销
异步编程和Continuation 以及语言的支持
- Task非常适合异步编程,因为它们支持Continuation(它对异步非常重要)
第十六讲里面TaskCompletionSource的例子
TaskCompletionSource是实现底层IO-Bound异步方法的一种标准方式 - 对于Compute-Bound方法,Task.Run会初始化绑定线程的并发
把task返回调用者,创建异步方法
异步编程的区别:目标是在调用图的较低位置来这样做
富客户端应用中,高级方法可以保留在UI线程和访问控制以及共享状态上,不会出现线程安全问题
同步版
static void Main(string[] args)
{
DisplayPrimeCounts();
//粗粒度调用
//Task.Run(() => DisplayPrimeCounts());
}
//相当于UI线程
static void DisplayPrimeCounts()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(GetPrimesCount(i * 1000000 + 2, 1000000) +
" primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
}
Console.WriteLine("Done!");
}
static 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));
}
结果
异步版,但是输出不是我们想要的结果
static void Main(string[] args)
{
DisplayPrimeCounts();
//需要阻塞线程
Console.ReadKey();
}
//相当于UI线程
static void DisplayPrimeCounts()
{
for (int i = 0; i < 10; i++)
{
var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
awaiter.OnCompleted(() => Console.WriteLine(awaiter.GetResult() + " primes between ... "));
}
Console.WriteLine("Done!");
}
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)));
}
结果
语言对异步的支持非常重要
- 上述例子
- 需要对task的执行序列化
例如Task B依赖于Task A的执行结果
上述例子为此,必须在continuation内部触发下一次循环
同步版
static void Main(string[] args)
{
DisplayPrimeCounts();
//需要阻塞线程
Console.ReadKey();
}
static void DisplayPrimeCounts()
{
DisplayPrimeCountsFrom(0);
}
static void DisplayPrimeCountsFrom(int i)
{
var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
//内部异步调用
awaiter.OnCompleted(() =>
{
Console.WriteLine(awaiter.GetResult() + " primes between ... ");
if (++i < 10)
{
DisplayPrimeCountsFrom(i);
}
else
{
Console.WriteLine("Done");
}
});
}
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)));
}
结果
异步版,利用状态机,太麻烦
class Program
{
static void Main(string[] args)
{
DisplayPrimeCountsAsync();
//需要阻塞线程
Console.ReadKey();
}
static Task DisplayPrimeCountsAsync()
{
var machine = new PrimesStateMachine();
machine.DisplayPrimeCountsFrom(0);
return machine.Task;
}
public 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)));
}
}
class PrimesStateMachine
{
TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
public Task Task
{
get { return _tcs.Task; }
}
public void DisplayPrimeCountsFrom(int i)
{
var awaiter = Program.GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine(awaiter.GetResult());
if (++i < 10)
{
DisplayPrimeCountsFrom(i);
}
else
{
Console.WriteLine("Done");
_tcs.SetResult(null);
}
});
}
}
结果
3、async和await
对于不想复杂的实现异步非常重要
原理:状态机
异步编程时,异步函数被编译为包含状态机的形式,使得代码在某处停留,等待某个任务完成
修改后的例子
static async Task Main(string[] args)
{
await DisplayPrimeCountsAsync();
}
async static Task DisplayPrimeCountsAsync()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000) + " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));
}
Console.WriteLine("Done");
}
//凡是返回Task的方法都可以用await进行调用
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)));
}
结果