常见概念
已经有多线程了,为什么还要异步
多线程与异步是不同的概念,异步并不意味着多线程,单线程同样可以异步,异步默认借助线程池,多线程经常阻塞,而异步要求不阻塞
多线程与异步的适用场景不同
多线程适合-适合CPU密集型操作,适合长期运行的任务,线程的创建与销毁开销较大,提供更底层的控制,操作线程,锁.,信号量等,线程不易于传参及返回,线程的代码书写较为繁琐
异步适合-适合IO密集型操作,适合短暂的小任务,避免线程阻塞,提高系统响应能力
什么是异步任务(Task)
Task是包含了异步任务的各种状态的一个引用类型,比如是否正在运行 完成 结果 报错等,另有ValueTask值类型版本
对于异步任务的抽象
开始异步任务后,当前线程并不会阻塞,而是看去做其他的事情
异步任务(默认)会结组线程池在其他线程上运行
获取结果后回到之前的状态
任务的结果
返回值为Task的方法表示异步任务没有返回值
返回值为Task<T>则表示有类型为T的返回值
异步方法(async Task)
将方法标记async后,可以在方法中使用await关键字
await关键字会等待异步任务的结束,并且获得结果
async+await会将方法包装成状态机,await类似于检查点-MoveNext方法会被底层调用,从而切换状态
async Task
返回值一定是Task类型,但是在其中可以使用await关键字
在其中写返回值可以直接写Task<T>中的T类型,不用包装成Task<T>
async Task的核心含义
async Task是 C# 中定义无返回值异步方法 的标准方式,两个关键字 / 类型各司其职:
部分 核心作用 async标记方法为 "异步方法",告诉编译器将方法编译为状态机 (支持 await暂停 / 恢复执行),仅作为 "标记",不直接执行异步逻辑Task表示一个 "无返回值的异步操作",是可等待对象(awaitable):调用方通过 await等待该异步操作完成,且能捕获方法内的异常通俗比喻:
async Task<T>就像 "点一份带小票的外卖":你(调用方)下单后拿到小票(Task<T>),不用等(非阻塞),可以做其他事;外卖做好后,凭小票(await)取到具体的外卖(T类型返回值)。- 对比
async Task(无返回值):相当于 "点一份无需小票的外卖",只需要知道 "送没送到",不需要拿到具体东西。
async= 给方法贴个 "可暂停" 标签,告诉编译器 "这个方法里有需要等待的操作,别一次性执行完";Task= 给调用方的 "任务凭证",调用方拿着这个凭证可以 "等任务做完",但任务本身没有具体结果要返回(比如 "异步写日志" 只需要知道写没写完,不需要返回值)。
async Task<T>
async Task<T>是定义有返回值异步方法 的标准方式,是日常开发中最常用的异步方法类型
部分 核心作用 async标记方法为异步方法,编译器将其编译为状态机,支持 await暂停 / 恢复执行;Task<T>表示 "有返回值的异步操作", T是返回值的类型(如Task<string>返回字符串、Task<int>返回整数);调用方通过await等待操作完成,并获取T类型的返回值。定义规则 :① 方法必须用
async修饰,返回类型为Task<T>(T是实际返回值类型);② 方法内部必须有await(否则是 "伪异步",编译器警告);③ 方法内部用return返回T类型的值(而非Task<T>,编译器会自动包装为Task<T>);④ 命名规范:后缀加Async(如GetDataAsync)。调用规则 :① 必须用
await调用,才能获取T类型的返回值;② 异常可通过try-catch捕获(异常会封装到Task<T>中,await时抛出)。基础用法:
csusing System; using System.Threading.Tasks; class AsyncTaskTDemo { // 主线程:async Task Main是入口方法 static async Task Main() { Console.WriteLine("开始异步获取用户信息..."); // 调用async Task<T>方法,await获取返回值 UserInfo user = await GetUserInfoAsync(1001); Console.WriteLine($"获取到用户信息:ID={user.Id},Name={user.Name}"); } // 定义async Task<T>方法:返回UserInfo类型的异步方法 static async Task<UserInfo> GetUserInfoAsync(int userId) { Console.WriteLine("模拟数据库查询(异步IO操作)..."); // 模拟异步IO操作(数据库查询/网络请求) await Task.Delay(1500); // 替代实际的异步查询 // 直接返回T类型(UserInfo),编译器自动包装为Task<UserInfo> return new UserInfo { Id = userId, Name = "张三", Age = 25 }; } // 自定义返回类型 class UserInfo { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } } }执行结果:
cs开始异步获取用户信息... 模拟数据库查询(异步IO操作)... (主线程不阻塞,1.5秒后) 获取到用户信息:ID=1001,Name=张三
async void
async void是特殊的无返回值异步方法 ,仅用于事件处理程序(如按钮点击、定时器触发),是异步方法中 "最危险" 的类型:核心规则(必须严格遵守)
- 仅用于事件处理程序 :如 WinForm/WPF 的按钮点击、ASP.NET Core 的事件回调,普通方法绝对不能用;
- 无法 await :调用
async void方法后,调用方无法等待其完成,也无法获取执行状态;- 异常处理特殊 :方法内的异常不会封装到
Task中,会直接抛到当前同步上下文(如 UI 线程 / 线程池),导致程序崩溃(无法通过try-catch捕获);- 生命周期不可控:调用方无法知道方法何时执行完成,可能引发资源未释放、数据不一致等问题。
部分 核心作用 async标记方法为异步方法,支持内部 await;void无返回值,且不可被 await(调用方无法等待其完成);异常会直接抛到线程池,导致程序崩溃。 定义规则 :① 方法必须用
async修饰,返回类型为Task<T>(T是实际返回值类型);② 方法内部必须有await(否则是 "伪异步",编译器警告);③ 方法内部用return返回T类型的值(而非Task<T>,编译器会自动包装为Task<T>);④ 命名规范:后缀加Async(如GetDataAsync)。调用规则 :① 必须用
await调用,才能获取T类型的返回值;② 异常可通过try-catch捕获(异常会封装到Task<T>中,await时抛出)。示例:
csusing System; using System.Threading.Tasks; class AsyncTaskTDemo { // 主线程:async Task Main是入口方法 static async Task Main() { Console.WriteLine("开始异步获取用户信息..."); // 调用async Task<T>方法,await获取返回值 UserInfo user = await GetUserInfoAsync(1001); Console.WriteLine($"获取到用户信息:ID={user.Id},Name={user.Name}"); } // 定义async Task<T>方法:返回UserInfo类型的异步方法 static async Task<UserInfo> GetUserInfoAsync(int userId) { Console.WriteLine("模拟数据库查询(异步IO操作)..."); // 模拟异步IO操作(数据库查询/网络请求) await Task.Delay(1500); // 替代实际的异步查询 // 直接返回T类型(UserInfo),编译器自动包装为Task<UserInfo> return new UserInfo { Id = userId, Name = "张三", Age = 25 }; } // 自定义返回类型 class UserInfo { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } } }关键:
async void事件处理程序中,必须内部捕获所有异常,否则一旦抛出未处理异常,程序会直接崩溃。
async Taskvsasync Task<T>vsasync void(核心对比)新手最容易混淆这三种异步方法类型,用表格清晰区分:
方法类型 返回值 核心特点 适用场景 异常处理方式 async Task无(仅 Task) 可 await、可捕获异常、无返回值 无返回值的异步操作(写日志、发邮件) await 时捕获异常 async Task<T>有(T 类型) 可 await、可捕获异常、有返回值 有返回值的异步操作(查数据库、读文件) await 时捕获异常,同时获取返回值 async void无(void) 不可 await、异常直接崩溃程序 仅事件处理程序(如按钮点击事件) 无法通过常规方式捕获,极易崩溃
异步编程具有传染性
一处async,处处async,几乎所有的自带方法都提供了异步的版本
重要思想:不阻塞
await会暂时释放当前线程,使得该线程可以执行其他工作,而不必阻塞线程直到异步操作完成
不要在异步方法里用任何方式阻塞当前线程
常见的阻塞情形
Task.Wait()&Task.Result:如果任务没有完成,则会阻塞当前线程,燃油导致死锁--Task.GetAwaiter().GerTesult()
Task.Delay() vs Thread.Sleep() : 后者会阻塞当前的线程,这与异步编程的理念不符合,前者是一个异步任务,会立即释放当前的线程
IO等操作的同步方法
其他繁重且耗时的任务
同步上下文
同步上下文是一种管理和协调线程的机制,允许开发者将代码的执行切换到特定的线程.
WinForms与WPF拥有同步上下文(UI线程),而控制台程序默认没有
ConfigureAwait(false) : 配置任务通过await方法结束后是否会回到原来的线程,默认为true,一般只有UI线程会采取这种策略
TaskScheduler
你可以把
TaskScheduler理解为任务调度员:
- .NET 中所有
Task的执行都由TaskScheduler接管(除非用Task.RunSynchronously强制同步执行)。- 它的核心职责是:接收待执行的 Task,根据自身规则分配线程(比如线程池线程、UI 线程、自定义线程),并管理 Task 的执行顺序。
- 不同的
TaskScheduler实现对应不同的调度规则,比如 UI 程序中要把更新 UI 的任务调度到主线程,后台计算任务调度到线程池。代码示例:
cs// 1. 引入基础系统类库,包含Console(控制台输出)、Environment(获取线程信息)等核心类 using System; // 2. 引入异步编程相关类库,包含Task、TaskScheduler等核心类型 using System.Threading.Tasks; // 3. 定义程序的主类(C#程序的代码必须放在类中) class Program { // 4. Main方法是C#控制台程序的入口点,这段代码的执行从这里开始,Main方法运行在**主线程**中 static void Main() { // 5. 创建并立即启动一个异步Task // Task.Run() 是创建后台任务的快捷方式,默认使用ThreadPoolTaskScheduler // 括号内的Lambda表达式 (() => { ... }) 是Task要执行的具体逻辑 Task task1 = Task.Run(() => { // 6. Task的执行逻辑:输出当前执行线程的ID,并标注"非主线程" // Environment.CurrentManagedThreadId:获取当前运行代码的线程唯一ID(整数) Console.WriteLine($"Task1 线程ID: {Environment.CurrentManagedThreadId}(非主线程)"); }); // 7. 主线程阻塞等待task1执行完成(同步等待) // 如果没有这行,主线程可能会直接退出,导致task1还没执行就结束程序,看不到Task1的输出 task1.Wait(); // 8. 主线程输出自己的线程ID,和Task1的线程ID做对比 Console.WriteLine($"主线程ID: {Environment.CurrentManagedThreadId}"); } }控制Task的调度方式和运行线程---
线程池线程 Default
当前线程 CurrentThread
单线程上下文STAThread
长时间运行线程 LongRunning
优先级、上下文、执行状态等
一发即忘(Fire-and-forget)
调用一个异步方法,但是并不使用await或阻塞的方式去等待他的结束
无法观察任务的状态
简单任务
如何创建异步任务
Task.Run()
你可以把
Task.Run()理解为:给.NET 线程池 "派一个活" ------ 它不会创建全新的线程,而是复用线程池中的空闲工作线程来执行你的代码,避免了频繁创建 / 销毁线程的性能开销,同时让代码脱离调用线程(比如主线程)异步执行,不阻塞调用方。
csusing WinFormsApp1; class Program { static async Task Main() { Helper.PrintThreadId("之前"); var res = await Task.Run(HeavyJob); Helper.PrintThreadId("完成"); Console.WriteLine(res); } static async Task<int> HeavyJob() { Helper.PrintThreadId("使用"); Thread.Sleep(1000); return 54; } }Task.Factory.StartNew()
Task.Factory.StartNew()是.NET 中创建并启动Task的底层 API ,核心功能是:接收要执行的代码逻辑,通过配置参数自定义任务的创建规则、取消策略、调度方式,然后立即启动任务(无需手动调用Start())。相当于Task.Run的进化版如何开启多个异步任务
1.并行开启多个异步任务(Task.WhenAll)
csvar 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(s=>s.Result).ToArray(); Console.WriteLine($"outputs数组内容:{string.Join(", ", outputs)}"); async Task<int> HeavyJob(int input) { await Task.Delay(10); return input * input; }2.按序开启多个异步任务(foreach + await)
如果任务之间有依赖(如任务 B 需要任务 A 的结果),或需要逐个执行,用这种方式(总耗时 = 所有任务耗时之和):
csstatic async Task Main() { var inputs = Enumerable.Range(1, 10).ToArray(); var outputs = new List<int>(); // 逐个开启任务,await等待当前任务完成后再开下一个 foreach (var input in inputs) { int result = await HeavyJob(input); // 按序执行,每10ms完成一个 outputs.Add(result); Console.WriteLine($"按序执行:输入{input} → 输出{result}"); } Console.WriteLine($"按序执行总结果:{string.Join(", ", outputs)}"); }3.进阶:带并发限制的多异步任务(避免资源耗尽)
如果开启成百上千个异步任务(如批量调用接口),直接用Task.WhenAll会导致线程池 / 网络连接耗尽,需限制同时运行的任务数(用SemaphoreSlim):
csstatic async Task Main() { var inputs = Enumerable.Range(1, 20).ToArray(); var outputs = new List<int>(); // 限制同时最多运行3个异步任务 var semaphore = new SemaphoreSlim(3); // 生成所有任务,但执行时受信号量限制 var tasks = inputs.Select(async input => { await semaphore.WaitAsync(); // 申请信号量,满了则等待 try { return await HeavyJob(input); // 执行任务 } finally { semaphore.Release(); // 释放信号量,让下一个任务执行 } }).ToArray(); await Task.WhenAll(tasks); outputs = tasks.Select(t => t.Result).ToList(); Console.WriteLine($"带限制的并行结果:{string.Join(", ", outputs)}"); }
常见误区
异步一定是多线程?
异步编程不必需要多线程来实现,可以用时间片轮转调度来实现
比如可以在单个线程上使用异步I/O或事件驱动的编程模型(EAp)
单线程异步:自动定好计时器,到时间之前先去做别的事情
多线程异步:将任务交给不同的线程,并由自己来进行指挥调度
异步方法一定要写成async Task?
async关键字知识用来配合await使用,从而将方法包装成状态机,本质上依然是Task,只不过提供了语法糖,并且函数体中可以直接return Task的泛型类型,接口中无法声明async Task
await一定会切换同步上下文?
在使用await关键字调用并等待一个异步任务时,异步方法不一定会立刻来到新的线程上,如果await了一个已经完成的任务,会直接获得结果
异步可以全面取代多线程?
异步编程与多线程有一定光系,但二者并不是可以完全互相代替
Task.Result一定会阻塞当前线程?
如果任务已经完成,那么可以直接得到结果
开启的异步任务一定不会阻塞当前线程?
await关键字不一定会立即释放当前线程,所以如果调用的异步方法中存在阻塞(如Thread.Sleep(0)),那么依旧会阻塞上下文对应的进程
异步编程中的同步机制
传统方法
Monitor(lock)、Mutex、Semaphore、EventWaitHandle
轻量型
SemaphoreSlim、ManualResetEventSlim
