异步编程是一种思路
异步相当于对线程池的封装
await相当于让另一个线程来干这个事
- 常见概念
- 已经有多线程了,为何还要异步
- 多线程与异步是不同的概念
- 多线程与异步的适用场景不同
- *多线程*
- *异步*
- 什么是异步任务(`Task`)
- 包含了异步任务的各种状态的一个引用类型
- 对于异步任务的抽象
- 任务的结果
- 异步方法(`async Task`)
- *`async Task`*
- 重要思想:不阻塞!
- *常见阻塞情形*
- 同步上下文
- *`ConfigureAwait(false)`*
- 一发即忘(Fire-and-forget)
- 简单任务
- 如何创建异步任务?
- `Task.Run()`
- `Task.Factory.StartNew()`
- `new Task` + `Task.Start()`
- 例子
- 如何同时开启多个异步任务?
- 任务如何取消?
- 任务超时如何实现?
- 在异步任务中汇报进度?
- 如何在同步方法中调用异步方法?
- 常见的误区
- 异步一定是多线程?
- 异步方法一定要写成async Task?
- await一定会切换同步上下文?
- 异步可以全面取代多线程?
- Task.Result 一定会阻塞当前线程?
- 开启的异步任务一定不会阻塞当前线程?
- 同步机制
- 传统方法
- 轻量型
- 并发集合
- 第三方库
常见概念
已经有多线程了,为何还要异步
多线程与异步是不同的概念
异步并不意味着多线程,单线程同样可以异步
异步默认借助线程池
多线程经常阻塞,而异步要求不阻塞
多线程与异步的适用场景不同
多线程
适合CPU密集型操作
适合长期运行的任务
线程的创建与销毁开销较大
提供更底层的控制,操作线程、锁、信号量等
线程不易于传参及返回
线程的代码书写较为繁琐
异步
适合IO密集型操作
适合短暂的小任务
避免线程阻塞,提高系统响应能力
什么是异步任务(Task
)
包含了异步任务的各种状态的一个引用类型
正在运行、完成、结果、报错等
public class TODO {public static event Func<object, string> foo;static void Main() {Task<string> task = new Task<string>((n) => {Thread.Sleep(1500);for (int i = 0; i < (int)n; i++) {Console.WriteLine("DONE {0}", i);}return "ok";}, 3);Console.WriteLine(task.Status);task.Start();Console.WriteLine(task.Status);Thread.Sleep(1000);Console.WriteLine(task.Status);Thread.Sleep(2000);Console.WriteLine(task.Status);Console.WriteLine(task.Result);}
}
输出结果
Created
WaitingToRun
Running
DONE 0
DONE 1
DONE 2
RanToCompletion
ok
另有ValueTask值类型版本
对于异步任务的抽象
开启异步任务后,当前线程并不会阻塞,而是可以去做其他事情
异步任务(默认)会借助线程池在其他线程上运行
获取结果后回到之前的状态
任务的结果
返回值为Task的方法表示异步任务没有返回值
返回值为Task<T>则表示有类型为T的返回值
异步方法(async Task
)
将方法标记async
后,可以在方法中使用await
关键字
await
关键字会等待异步任务的结束,并获得结果
async
+ await
会将方法包装成状态机,await
类似于检查点
MoveNext方法会被底层调用,从而切换状态
async Task
返回值依旧是Task类型,但是在其中可以使用await关键字
在其中写返回值可以直接写Task<T> 中的T类型,不用包装成Task<T>
async void
同样是状态机,但缺少记录状态的Task对象
无法聚合异常(Aggregate Exception),需要谨慎处理异常
几乎只用于对于事件的注册
*异步编程具有传染性(Contagious)
*
一处async
,处处async
几乎所有自带方法都提供了异步的版本
重要思想:不阻塞!
await
会暂时释放当前线程,使得该线程可以执行其他工作,而不必阻塞线程直到异步操作完成
不要在异步方法里用任何方式阻塞当前线程
常见阻塞情形
Task.Wait()
& Task.Result
如果任务没有完成,则会阻塞当前线程,容易导致死锁Task.GetAwaiter().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.L ongRunning
Task.Run相当于简化版
new Task
+ Task.Start()
很少有创建一个Task却没有让他立刻开始的
例子
public class TODO {static async Task Main() {Console.WriteLine(12);Console.WriteLine("ThreadId" + Environment.CurrentManagedThreadId.ToString());var task = await Task.Run(heavyJob);//Console.WriteLine(task);Console.WriteLine(123);}public static int heavyJob() {Console.WriteLine("ThreadId" + Environment.CurrentManagedThreadId.ToString());Thread.Sleep(10);return 1;}}
如何同时开启多个异步任务?
public class TODO {static async Task Main() {var inputs = Enumerable.Range(10, 10).ToArray();var tasks = new List<Task<int>>();Console.WriteLine(Environment.CurrentManagedThreadId);foreach (var input in inputs) {tasks.Add(foo(input));}await Task.WhenAll(tasks);var outputs = tasks.Select(x => x.Result).ToArray();foreach (var output in outputs) {Console.WriteLine(output);}}public static async Task<int> foo(int input) {await Task.Delay(5000);return input * 2;}
}
任务如何取消?
CancellationTokenSource
+ CancellationToken
public class TODO {static async Task Main() {var cts = new CancellationTokenSource();try {var task = Task.Delay(100000, cts.Token);Thread.Sleep(2000);cts.Cancel();//抛出异常await task;} catch (TaskCanceledException) {Console.WriteLine("ss");} finally {cts.Cancel();}}
}
OperationCanceledException
& TaskCanceledException
推荐异步方法都带上CancellationToken
这一传参
你自己写了异步方法却不支持传入这个————我可以不用,但不能没有
任务超时如何实现?
在异步任务中汇报进度?
如何在同步方法中调用异步方法?
常见的误区
异步一定是多线程?
异步编程不必需要多线程来实现
比如可以在单个线程上使用异步I/O 或事件驱动的编程模型(EAP)
单线程异步:自己定好计时器,到时间之前先去做别的事情
多线程异步:将任务交给不同的线程,并由自己来进行指挥调度
异步方法一定要写成async Task?
async关键字只是用来配合await 使用,从而将方法包装为状态机
本质上仍然是Task,只不过提供了语法糖,并且函数体中可以直接return Task的泛型类型
接口中无法声明async Task
await一定会切换同步上下文?
在使用await关键字调用并等待一个异步任务时,异步方法不一定会立刻来到新的线程上
如果await了一个已经完成的任务(包括Task.Delay(0)),会直接获得结果
异步可以全面取代多线程?
异步编程与多线程有一定关系,但两者并不是可以完全互相替代
Task.Result 一定会阻塞当前线程?
如果任务已经完成,那么Task.Result 可以直接得到结果
开启的异步任务一定不会阻塞当前线程?
await关键字不一定会立刻释放当前线程,所以如果调用的异步方法中存在阻塞(如Thread.Sleep(O))
那么依旧会阻塞当前上下文对应的线程
同步机制
传统方法
Monitor(lock)
Mutex
Semaphore
EventWaitHandle
轻量型
所有只有SemaphoreSlim不阻塞
ManualResetEventSlim
并发集合
第三方库
AsyncManulResetEvent
Miccrosoft.VisualStudio.Threading
AsyncLock
Nito.AsyncEx