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 关键字,而是直接返回。
也就是说,这个异步任务自始至终都是在同一个线程上执行的。

posted @ 2025-06-10 15:29  星渐渐被你吸引  阅读(56)  评论(1)    收藏  举报