异步与多线程是不同的概念
异步并不意味着多线程,单线程同样可以异步。
异步默认借助线程池。
多线程经常会有阻塞的操作,而异步要求不阻塞。
异步与多线程适用场景不同
多线程:
- 适合CPU密集型操作
- 适合长期运行的任务
- 线程的创建与销毁的开销是比较大的
- 提供更底层的控制,操作线程、锁、信号量等
- 线程不利于传参和返回
- 线程的代码书写较为繁琐
异步:
- 适合IO密集型操作
- 适合短暂的小任务
- 避免线程阻塞,提高系统的响应能力
什么是异步任务(Task)
开启异步任务之后,当前线程并不会阻塞,而是可以去做其他的事情。
异步任务默认会借助线程池在其他线程上运行。
获取结果后回到之前的状态。
任务的结果:
- 返回值为
Task
的方法表示异步任务没有返回值 - 返回值为
Task<T>
的方法表示有类型为T的返回值
异步方法(async Task)
- 将方法标记
async
后,可以在方法中使用关键字await
await
关键字会等待异步任务的结束,并获得结果async
+await
会将方法包装成状态机,await
类似于检查点async Task
的返回值依旧是Task类型,但是在其中可以使用await关键字。在其中写返回值可以直接写Task<T>
中的T类型。
重要思想:不阻塞
await
会暂时释放当前线程,使得该线程可以执行其他工作,而不必阻塞线程直到异步操作完成。
不要在异步方法里用任何方式阻塞当前线程。
为什么在异步编程中,不使用Thread.Sleep,而是Task.Delay
Task.Delay是一个不阻塞的异步任务,而Thread.Sleep则真正的阻塞了当前线程一段时间
同步上下文
- ConfigureAwait(false),配置任务通过await方法结束后是否回到原来的线程,默认是true。
- TaskScheduler,控制Task的调度方式和运行线程。就像长时间运行线程LongRunning,因为Task默认一般处理比较短的任务,如果任务耗时比较长,可以标记成“长时间运行线程”
如何创建异步任务
- Task.Run()
- Task.Factory.StartNew(),提供更多功能,比如TaskCreationOptions.LongRunning,上一个Run是该方法的简略版
- new Task + Task.Start()
如何同时开启多个异步任务
var inputs = Enumerable.Range(1, 10).ToArray();
var tasks = new List<Task<int>>();
foreach(var input in inputs)
{
tasks.Add(HeavyJob(input));
}
await Task.WhenAll(tasks);
var outputs = tasks.Select(x => x.Result).ToArray();
outputs.Dump();
async Task<int> HeavyJob(int input)
{
await Task.Delay(1000);
return input*input;
}
异步任务如何取消
使用CancellationToken
推荐异步方法都带上CancellationToken
var cts = new CancellationTokenSource();
try
{
var task = Task.Delay(10000, cts.Token);
Thread.Sleep(2000);
cts.Cancel();
await task;
}
catch(TaskCanceledException)
{
"Task.Canceled".Dump();
}
finally
{
cts.Dispose();
}
异步一定是多线程?
异步编程不必需要多线程来实现:时间片轮转调度。
单线程异步:自己定好计时器,到时间之前先去做别的事情。
多线程异步:将任务交给不同的线程,并有自己来指挥调度。
异步方法一定要写成async Task吗
不一定。async只是配合await来使用,将方法包装成状态机。
开启的异步任务一定不会阻塞当前线程?
await关键字不一定会立即释放当前线程,所以如果调用的异步方法中存在阻塞(如Thread.Sleep(0)),那么依旧会阻塞当前上下文对应的线程。