C# 异步编程知识总结
常见概念
已经有多线程了,为何还要异步
多线程与异步是不同的概念
- 异步并不意味着多线程,单线程同样可以异步(CPU调度)
- 异步默认借助多线程
- 多线程经常阻塞,而异步要求不阻塞
多线程与异步的适用场景不同
多线程
- 适合CPU密集型操作
- 适合长期运行的任务
- 线程的创建与销毁,开销较大
- 提供更底层的控制,操作线程、锁、信号量等
- 线程不易于传参和返回
- 线程的代码书写比较繁琐
异步
- 适合IO密集型操作
- 适合短暂的小任务
- 避免线程阻塞,提高系统响应能力
什么是异步任务(Task)
概念
- 包含了异步任务的各种状态的一个引用类型(Class):正在运行、完成、结果、报错等。
- 另有
ValueTask值类型版本。
对异步任务的抽象
- 开启异步任务后,当前线程并不会阻塞,而是可以去做其他事情。
- 异步任务(默认)会借助线程池在其他线程上运行
- 获取结果后回到之前的状态
任务的结果
- 返回值为
Task的方法表示异步任务没有返回值 - 返回值为
Task<T>则表示有类型为T的返回值
异步方法async Task
- 将方法标记为async,可以在方法中使用await关键字
- await关键字会等待异步任务的结束,并获得结果。(它其实是在阻塞当前任务所使用的线程,不能使用Task.Result(这是阻塞当前线程))
- async+await会将方法包装成状态机(StateMachine),await类似于检查点(分为多个阶段)(MoveNext方法会被底层调用,从而切换状态)
- async Task
- 返回值依旧是Task类型,但是在其中可以使用await关键字
- 在其中写返回值可以直接写
Task<T>中的T类型,不用包装成Task<T>
- async void
- 同样是状态机,但是缺少记录状态的Task对象
- 内部仍然可以使用await
- 外部调用时无法使用await,只能直接调用
- 内部的异常无法被catch到(无法聚合异常Aggregate Exception),需要谨慎处理异常。
- 几乎只用于对事件的注册。
- 异步方法具有传染性(一处async,处处async)。几乎所有自带方法都提供了异步方法。
重要思想:不阻塞!
- await会暂时释放掉当前的线程,使得该线程可以执行其他工作,而不必阻塞线程直到异步操作完成。
- 不要在异步方法里用任何方式阻塞当前线程。
- 常见阻塞情形:
- Task.Wait() & Task.Result,如果任务没有完成,则会阻塞当前线程,容易导致死锁。Task.GetAwater().GetResult()-不会将Exception包装为AggregateException。
- Task.Delay() vs Thread.Sleep()
- IO等操作的同步方法
- 其他繁重且耗时的任务
同步上下文
- 一种管理和协调线程的机制,允许开发者将代码的执行切换到特定的线程。
- WinForms 与 WPF 拥有同步上下文(UI线程)。而控制台程序默认没有。
- ConfigureAwait(false):配置任务通过await方法结束后是否会到原来的线程,默认为true。一般只有UI线程会采用这种策略。
- TaskScheduler:控制Task的调度方式和运行线程
- 线程池线程Default
- 当前线程CurrentThread
- 单线程上下文STAThread
- 长时间运行线程LongRunning
- 优先级、上下文、执行状态等
一发即忘(Fire-and-forget)
- 调用一个异步方法,但是并不使用await或阻塞的方式去等待它的结束。
- 无法观察任务的状态(是否完成、是否报错等)
简单任务
如何创建异步任务?
- Task.Run()
- Task.Factory.StartNew()
- 提供更多功能:比如TaskCreationOptions.LongRunning
- Task.Run相当于简化版
- new Task()+Task.Start()
如何同时开启多个异步任务?
- 将多个创建好的异步任务添加到var tasks = List< Task>中,调用await Task.WhenAll(tasks);多个异步任务就同时开始直到所有的任务结束,再遍历task.Result可获得结果。
- await Task.WhenAny(tasks),只要有任何一个Task结束,就结束等待,认为完成了。
异步任务如何取消
- 传入CancellationTokenSource cts的token至Task任务,在合适的实机调用cts.Cancel();在外部捕获TaskCanceledException & OperationCanceledException。
异步任务超时如何实现
- 通过Task.WhenAny(),判断率先完成的Task是否是目标Task。
- await task.WaitAsync(time);若catch到TimeoutException则目标Task超时。
- 异步任务超时后,记得取消掉目标Task。
在异步任务中汇报进度
在异步方法中调用IProgress的Report方法将进度value作为参数,触发IProgress自带委托方法进行进度显示。
如何在同步方法中调用异步方法
FooAsync().GetAwaiter().GetResult();
如何实现多个异步任务的同时完成
安装Nuget包:Microsoft.VisualStudio.Threading,使用AsyncBarrier实现。
常见误区
异步一定是多线程?
- 异步编程不一定需要多线程来实现,比如:时间片轮转调度。
- 比如可以在单个线程上使用异步I/O或事件驱动的编程模型(EAP)。
- 单线程异步:自己定好计时器,到时间之前先去做别的事。
- 多线程异步:将任务交给不同的线程,并由自己来进行指挥调度。
异步方法一定要写成async Task?
- async关键字只是用来配合await使用,从而将方法包装成状态机
- 本质上仍然是Task,只不过提供了语法糖,并且函数体中可以直接return Task的泛型类型
- 接口中无法声明async Task
- async void
await是否一定会切换上下文?
同步机制
传统方法(都不能用,因为是阻塞的)
- 异步方法不能用lock语句包装await语句(lock底层的Monitor必须在同一个线程上Enter和Exit)。
- Monitor(lock)
- Mutex
- Semaphore
- EventWaitHandle
轻量型
- SemaphoreSlim
并发集合
第三方库
其它
ValueTask
这种在异步任务中直接返回一个值的情况,我们称之为“同步完成”,或者“返回同步结果”。
线程进入这个异步任务后,并没有碰到 await 关键字,而是直接返回。
也就是说,这个异步任务自始至终都是在同一个线程上执行的。

浙公网安备 33010602011771号