异步编程的概念及核心理念

常见概念

  1. 已经有多线程了,为什么还要异步?
    多线程与异步是不同的概念,异步并不意味着多线程,单线程同时可以异步。
    异步默认借助线程池,多线程经常阻塞,而异步要求不阻塞。
    多线程和异步的使用场景不同:
  • 多线程:CPU密集型,长期运行的任务,线程的创建与销毁的开销都比较大
    提供更多底层控制,操作线程,锁,信号量等,线程不易于传参及返回,线程
    的代码书写较为繁琐
  • 异步:适合IO密集型操作,适合短暂的小任务,避免线程阻塞,提高系统的响应能力
  1. 什么是异步任务(Task)
    包含了异步任务的各种状态的一个引用类型,正在运行、完成、结果和报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var task = new Task<string>(() =>
{
// Simulate some work
System.Threading.Thread.Sleep(1500);
return "Task completed!";
});

Console.WriteLine(task.Status.ToString());
task.Start();
Console.WriteLine(task.Status.ToString());
Thread.Sleep(1000);
Console.WriteLine(task.Status.ToString());
Thread.Sleep(2000);
Console.WriteLine(task.Status.ToString());
Console.WriteLine(task.Result);
1
2
3
4
5
6
7
PS D:\INFORMATION\dotnet\ConsoleApp> dotnet run
Created
WaitingToRun
Running
RanToCompletion
Task completed!
PS D:\INFORMATION\dotnet\ConsoleApp>

同时还是对于异步编程的抽象,开启异步任务后,当前线程并不会阻塞,而是可以去做其他的事情,异步任务(默认)会借助线程池在其他线程上运行,获取结果后回到之前的状态。
任务的结果:
返回值为Task的方法表示异步任务没有返回值
返回值为Task泛型类型则表示有类型为T的返回值
3. 异步方法(async Task)

  • 将方法标记为async后,可以在方法中使用await关键字
  • await关键字会等待异步任务的结果,并获得结果
  • async + await 会将方法包装成状态机,await类似于检查点,MoveNext方法会被底层调用,从而切换状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using System.ComponentModel.Design;
using System.Runtime.CompilerServices;

async Task Main()
{
Helper.PrintThreadId("Main Start");
await FooAsync();
Helper.PrintThreadId("Main End");
}

async Task FooAsync()
{
Helper.PrintThreadId("Before");
await Task.Delay(1000);
Helper.PrintThreadId("After");
}

class Helper
{
private static int index = 1;
public static void PrintThreadId(string? message = null, [CallerMemberName] string? memberName = null)
{
var title = $"{index}:{memberName}";
if (!string.IsNullOrEmpty(message))
title += $" @ {message}";

Console.WriteLine(Environment.CurrentManagedThreadId.ToString() +
$" - {title}");
Interlocked.Increment(ref index);

}
}
  • 一发即忘:调用一个异步方法,但是并不使用 await 或阻塞的方法去等待它的结果,无法观察任务的状态(是否完成,是否报错等)

简单任务

  1. 如何创建异步任务?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Task.Factory.StartNew()

Task.Run() // 相当于StartNew的简化版
new Task + Task.Start()

using System.Threading.Tasks;

int HeavyJob()
{
Thread.Sleep(1000);
return 1;
}

async Task Main()
{
var res = await Task.Factory.StartNew(HeavyJob);
var res = await Task.Run(HeavyJob);
Console.WriteLine($"After type: {res.GetType()}");
}
  1. 如何开启多个任务?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var inputs = Enumerable.Range(1, 10)
.ToArray();

var outputs = new List<int>();

// 单个task处理
foreach (var input in inputs)
{
var output = await HeavyJob(input);
outputs.Add(output);
}

// 多taskchuli
var tasks = new List<Task<int>>();
foreach (var input in inputs)
{
tasks.Add(HeavyJob(input));
}
await Task.WhenAll(tasks);
outputs = tasks.Select(x => x.Result).ToList();

Console.WriteLine("Outputs: " + string.Join(", ", outputs));


async Task<int> HeavyJob(int input)
{
await Task.Delay(100); // Simulate a heavy job
return input * 2;
}
  1. 异步任务如何取消
    CancellationTokenSource + CancellationToken
    OperationCanceledException & TaskCanceledException
    推荐异步方法都带上CancellationToken这一个传参,可以不用但不能没有
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var cts = new CancellationTokenSource();

try
{
var task = Task.Delay(100000, cts.Token);

Thread.Sleep(1000);
cts.Cancel();

await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was canceled.");
}
finally
{
cts.Dispose();
}

常见误区

  1. 异步一定是多线程?
    异步编程不必要多线程来实现-时间片轮换调度
    比如可以在单个线程上使用异步I/O或事件驱动的编程模型(EAP)
    单线程异步:自己定好计时器,到时间之前先去做别的事情
    多线程异步:将任务交给不同的线程,并由自己来进行指挥调度
  2. 异步方法一定要写成 async Task?
    async 关键字只是用来配合await使用,从而将方法包装成为状态机
    本质上乃然是Task,只不过提供了语法糖,并且行数体中可以直接return Task的泛型类型
    接口中无法声明async Task
  3. await 一定会切换同步上下文么?
    在使用await关键字调用并等待一个异步任务时,异步方法不一定会立即来到新的线程上,如果await 了一个已经完成的任务(包括Task.Delay(0)),会直接获取结果。
  4. 异步可以全面取代多线程么?
    异步编程与多线程有一定关系,但两者并不是可以完全相互代替的。