异步与多线程是不同的概念

异步并不意味着多线程,单线程同样可以异步。
异步默认借助线程池。
多线程经常会有阻塞的操作,而异步要求不阻塞。

异步与多线程适用场景不同

多线程:

  • 适合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)),那么依旧会阻塞当前上下文对应的线程。