进程 | 线程 | Task |async—await
进程是操作系统分配资源的基本单位,代表一个正在运行的应用程序实例。每个进程拥有独立的内存空间、全局变量和系统资源(如文件句柄、网络连接等)线程是进程内的执行单元,是CPU调度的基本单位。一个进程可包含多个线程,共享进程的内存和资源。.NET中通过 Thread、ThreadPool、Task 实现多线程,合理选择可提升性能。进程间隔离性强但开销大,线程共享资源但需同步机制。进程是资源容器,线程
文章目录
一、进程与线程的核心概念
1、进程:Process
- 定义: 进程是操作系统分配资源的基本单位,代表一个正在运行的应用程序实例。每个进程拥有独立的内存空间、全局变量和系统资源(如文件句柄、网络连接等)
- 特点:
进程间完全隔离,崩溃不会影响其他进程
创建和销毁开销较大(需分配独立内存)
进程间通信需通过IPC机制(如管道、消息队列) - 生命周期:
创建(Process.Start)→ 加载 CLR(公共语言运行库)→ 运行 Main → 卸载 CLR → 退出(ExitCode) - 托管进程:
双击一个 .exe → Windows 创建一个 Windows 进程 → CLR 被加载(mscoree.dll → clr.dll)→ 创建一个 默认 AppDomain → JIT 把 IL 编译成本机代码开始执行 Main。
一个托管进程里可以加载多个 CLR 版本(罕见),但通常只加载一个 CLR。
2、线程:Thread
-
定义: 线程是进程内的执行单元,是CPU调度的基本单位。一个进程可包含多个线程,共享进程的内存和资源。线程是操作系统可调度的最小执行单元,拥有 1 MB 默认栈空间、寄存器上下文、TLS(线程本地存储)等。
-
特点:
线程间直接共享数据(如全局变量)
创建和切换开销小(仅需分配栈和上下文)
线程崩溃可能导致整个进程终止 -
区别对比
特性 进程 线程 资源分配 独立内存空间 共享进程内存 隔离性 完全隔离 共享资源 创建开销 高(需操作系统分配资源) 低(复用进程资源) 通信方式 IPC(管道、Socket等) 直接内存共享 并发性 低(依赖多核CPU) 高(单核即可实现并发) -
多线程与多进程
维度 多进程 多线程 适用任务 CPU密集型 I/O密集型 资源开销 高(独立内存) 低(共享内存) CPU密集型与I/O密集型的核心区别与典型示例
CPU密集型任务:适用多进程 I/O密集型任务:适用多线程 科学计算(数值模拟):求解偏微分方程,涉及万亿次浮点运算 HTTP请求处理:同时发起1000个API请求 视频编码/解码:帧间预测、熵编码等复杂算法 异步文件读取:读取大型日志文件(10GB) 机器学习模型训练 数据库查询 / 更新:并行查询 / 更新100个数据库记录 密码暴力破解:尝试2^256种密钥组合,依赖CPU并行计算 文件批量复制 / 备份:读写磁盘文件 3D图形渲染:光线与物体交互的物理模拟 日志收集与分析:读取大量日志文件 数字信号处理:5G通信中的OFDM调制解调 消息队列处理:从RabbitMQ队列消费消息 游戏物理引擎:碰撞检测、刚体运动方程数值积分 API 接口服务:等待外部 API 返回结果 图像识别算法(如特征提取、模式匹配) FTP 文件上传下载:网络传输文件 复杂数学运算(如矩阵运算、傅里叶变换) 邮件发送 / 接收服务:等待 SMTP/IMAP 服务器响应 压缩 / 解压缩算法(如高压缩率的 7Z 压缩过程) 网页爬虫:频繁请求网页并等待响应 -
总结
进程是资源容器,线程是执行单元。
进程间隔离性强但开销大,线程共享资源但需同步机制。
.NET中通过 Thread、ThreadPool、Task 实现多线程,合理选择可提升性能
二、线程:Thread
-
适用场景: 需要长时间运行的后台线程:如:监控服务、心跳检测、日志轮询等
-
什么是“长时间运行的任务”(Long-running Task)?
定义:执行时间较长、可能持续数秒、数分钟甚至更久,或持续运行的任务
长时间运行的任务”和短时间任务并没有一个绝对的、以秒为单位的硬性标准(比如“超过5秒就是长时间”),而是根据线程池的设计目标和系统上下文来判断的。执行时间参考:超过 1 秒 的任务通常就被认为是“长时间运行”的候选
执行时间 判断 < 100ms 明确是短任务 ✅ 100ms ~ 1秒 视情况而定,通常仍算短任务 > 1秒 建议考虑是否为“长时间运行” ⚠️ 持续运行(无限循环) 明确是长时间运行 ❗✅
1、线程执行无参方法
-
案例
public static void Main() { Console.WriteLine("主线程: 线程已启动,继续执行..." + " 线程ID:" + Environment.CurrentManagedThreadId); // 1. 创建一个新线程,让这个线程去执行DoWork()这个方法。(注意DoWork这个方法不要括号) // ThreadStart: 这是一个委托类型,它指向一个没有参数且没有返回值的方法: public delegate void ThreadStart(); Thread thread = new Thread(new ThreadStart(DoWork)); thread.Start(); // 启动线程 thread.Join(); // 等待线程结束(可选) var threadId = thread.ManagedThreadId.ToString(); // 线程ID //简写方式 Thread t = new Thread(DoWork); t.Start(); // 其实就是将new ThreadStart()省略掉了。因为委托容许我们这么简写:比如 // public delegate void Mydel(); // Mydel=new Mydel() 这段可以简写成Mydel=DoWork ;因为编译器自动给我们完成了MyDelegate md = new MyDelegate(DoWork); } // 新线程将执行的方法 public static void DoWork() { Console.WriteLine($"子线程:" + " 线程ID:" + Environment.CurrentManagedThreadId); Thread.Sleep(100); // 模拟耗时操作 Console.WriteLine("子线程: 任务完成。"); }
你也可以使用匿名方法或 Lambda 表达式来简化代码,使其更紧凑:
Thread thread = new Thread(() => { Console.WriteLine($"子线程:" + " 线程ID:" + Environment.CurrentManagedThreadId); Thread.Sleep(100); // 模拟耗时操作 }); thread.Start();
2、线程执行带一个参数的方法
- 案例
public static void Main() { Console.WriteLine("主线程: 线程已启动,继续执行..." + " 线程ID:" + Environment.CurrentManagedThreadId); //ParameterizedThreadStart 是一个委托,它指向一个接受单个 object 类型参数且无返回值的方法 ParameterizedThreadStart threadStart = new ParameterizedThreadStart(DoWork); //创建线程并传入带一个参数的方法:threadStart Thread thread = new Thread(threadStart); thread.Start("张三丰"); //DoWork方法的参数写在这里 } public static void DoWork(object? name) { Console.WriteLine($"我是一个有参数的方法, 我叫:{name},线程ID:" + Environment.CurrentManagedThreadId); Thread.Sleep(100); // 模拟耗时操作 Console.WriteLine("子线程: 任务完成。"); }
3、线程执行带多个参数的方法
- ParameterizedThreadStart 只能接受一个 object 类型的参数。如果需要传递多个参数,可以将它们封装在一个对象中
public class User { public string Namge { get; set; } public int Age { get; set; } } public static void Main() { //ParameterizedThreadStart 是一个委托,它指向一个接受单个 object 类型参数且无返回值的方法 ParameterizedThreadStart parameterized = new ParameterizedThreadStart(WorkMethod); User user = new User() { Namge = "lily", Age = 18 }; //线程执行一个带一个参数的委托(方法) Thread thread = new Thread(parameterized); thread.Start(user); //WorkMethod方法的参数写在这里 //简写1 Thread thread1 = new Thread(WorkMethod); thread1.Start(user); //WorkMethod方法的参数写在这里 //简写2:使用 Lambda 直接捕获参数,避免委托类型转换 Thread thread2 = new Thread(() => { WorkMethod(user); }); thread2.Start(); } public static void WorkMethod(object? data) { if (data is User user) { Console.WriteLine($"您好,我叫{user.Namge},我今年{user.Age}岁了"); Thread.Sleep(100); } }
4、线程执行带返回值的方法
案例1:使用BackgroundWorker 类
- 原理: 通过其事件模型实现线程间通信和结果传递
public class User { public required string Namge { get; set; } public int Age { get; set; } } internal class Program { public static void Main() { //创建一个后台执行者对象(其实就是BackgroundWorker这个类内部帮我们封装了一个线程) BackgroundWorker worker = new BackgroundWorker(); //worker.WorkerSupportsCancellation = true; // 支持取消操作 //worker.WorkerReportsProgress = true; // 支持进度报告 //绑定后台执行的方法(在独立线程中运行):即:线程需要执行的方法 worker.DoWork += new DoWorkEventHandler(DoWorkHandler); //worker.DoWork += DoWorkHandler; 简写 //绑定后台操作完成后的回调事件(在主线程执行):即:当程序执行完毕后触发的事件 worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(CompletedHandler); //worker.RunWorkerCompleted += CompletedHandler; 简写 //启动后台线程并传递User对象作为参数 backWorkder.RunWorkerAsync(new User { Namge = "张三", Age = 18 }); //因为RunWorkerAsync的参数只有一个object类型,如果需要传递多个参数,可以将它们封装在一个对象中 Console.ReadKey(); } //创建要在后台执行的事件 (在事件里执行方法): 将方法的返回值存入e.Result中 //这个事件就相当于线程要执行的方法 static void DoWorkHandler(object? sender, DoWorkEventArgs e) { //e.Cancel = false;//指示后台任务是否被用户主动取消: true表示任务已经取消,false表示任务未被取消 if (e.Argument is User user) { e.Result = $"您好,我叫{user.Namge},我今年{user.Age}岁了"; } } //创建当后台方法执行完毕后触发的事件:获取返回值 //当方法执行完毕后,通过这个事件来获取方法的返回值。 static void CompletedHandler(object? sender, RunWorkerCompletedEventArgs e) { Console.WriteLine("MybacckWorker_DoWork事件的返回值是:" + e.Result); } }
案例2:封装共享字段(通用方法)
- 原理: 将方法封装到类中,通过字段传递参数和返回值,由主线程检查状态或等待完成。
class Worker { public int Result; // 公共字段,用于存储计算结果 public void Calculate() { // 计算方法,将在后台线程中执行 Thread.Sleep(1000); // 模拟耗时操作(延迟1秒) Result = 50; // 设置计算结果(赋值给Result字段) } } // 启动线程 var worker = new Worker(); Thread thread = new Thread(worker.Calculate); thread.Start(); // 等待线程完成 thread.Join(); Console.WriteLine(worker.Result); //输出:50
三、线程池:ThreadPool
1、简介及适用场景
-
简介: ThreadPool 是一个用于管理和复用线程的机制,它可以帮助开发者更高效地处理多线程任务,避免频繁创建和销毁线程带来的性能开销
-
适用场景: 短期轻量任务 如:HTTP 请求处理、文件批量读写(每个任务 < 1秒)
适用于短时、频繁的任务,长时间运行的任务会占用线程池资源,可能导致性能问题。
2、用法案例
-
案例1:不带参数的方法
public static void Main() { // 将一个方法加入线程池队列 // QueueUserWorkItem: 这个方法将你的DoWork方法放入线程池队列,当有可用的线程时,就会执行它。 ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork)); Console.WriteLine("主线程: 任务已排队..."); Thread.Sleep(1000); // 等待一段时间,让线程池中的任务有机会执行 } public static void DoWork(object state) { Console.WriteLine("线程池线程: 任务开始执行。"); // ... 执行你的任务 }
-
案例2:带一个参数的lambda表达式
public static void Main() { int taskId = 1; ThreadPool.QueueUserWorkItem(state => { // state参数接收传递的值 int id = (int)state; Console.WriteLine($"任务 {id} 开始执行 (线程ID: {Thread.CurrentThread.ManagedThreadId})"); // 模拟任务执行 Thread.Sleep(1000); Console.WriteLine($"任务 {id} 执行完成"); }, taskId); // 这里的taskId将作为state参数传递给lambda }
-
案例3:带多个参数的lambda表达式
public static void Main() { // 案例:传递多个参数(使用匿名类型或自定义类) var taskData = new { Id = 2, Message = "这是一个带消息的任务", Delay = 1500 }; ThreadPool.QueueUserWorkItem(state => { // 将state转换为匿名类型 var data = (dynamic)state; Console.WriteLine($"任务 {data.Id} 开始执行,消息: {data.Message} (线程ID: {Thread.CurrentThread.ManagedThreadId})"); // 模拟任务执行 Thread.Sleep(data.Delay); Console.WriteLine($"任务 {data.Id} 执行完成"); }, taskData); // 传递匿名对象作为参数 }
-
案例4:带多个参数的方法
public class TaskData { public int Id { get; set; } public string? Name { get; set; } public int Duration { get; set; } } public static void Main() { // 案例3:传递自定义对象作为参数 var customData = new TaskData { Id = 3, Name = "自定义数据任务", Duration = 2000 }; ThreadPool.QueueUserWorkItem(ProcessTask, customData); // 等待所有任务完成(实际开发中需使用更可靠的同步机制) Thread.Sleep(3000); Console.WriteLine($"主线程结束 (ID: {Thread.CurrentThread.ManagedThreadId})"); } static void ProcessTask(object? state) { var data = (state ?? new TaskData()) as TaskData; // 提供默认实例 Console.WriteLine($"任务 {data?.Id} ({data?.Name}) 开始执行 (线程ID: {Thread.CurrentThread.ManagedThreadId})"); // 模拟任务执行 Thread.Sleep(data?.Duration ?? 1000); Console.WriteLine($"任务 {data?.Id} ({data?.Name}) 执行完成"); }
-
案例5:定时任务调度: ThreadPool.RegisterWaitForSingleObject
场景: 周期性执行任务(如心跳检测)。
实现: ThreadPool.RegisterWaitForSingleObject实现定时回调。替代 System.Timers.Timer,底层依赖线程池关键机制解析
1、AutoResetEvent的作用
初始状态为 false(未发出信号),使等待操作进入阻塞状态
核心技巧:此代码故意不使用 Set()触发事件,而是依赖 RegisterWaitForSingleObject的超时机制周期性触发回调。每次超时后,AutoResetEvent会自动重置(AutoReset特性),为下一次等待做准备2、RegisterWaitForSingleObject的工作流程
首次注册:立即开始等待 waitHandle的信号或超时(5秒)。
超时触发:因 waitHandle始终未被 Set(),5秒后因超时执行回调(timeout=true)。
重复执行:参数 true表示超时后自动重新开始等待,形成周期任务典型应用场景 如心跳检测、缓存刷新、日志批量处理。
public static void Main() { // 初始状态设为 false(无信号),确保依赖超时触发 var waitHandle = new AutoResetEvent(false); // 注册定时任务(重复执行) RegisteredWaitHandle rwh = ThreadPool.RegisterWaitForSingleObject( waitHandle, //定义事件触发或超时时的回调逻辑 (state, timeout) => { if (timeout)//为true表示:因超时触发,即:第四个参数设置的超时时间,过了超时时间就会触发 { try { Console.WriteLine($"定时任务执行于 {DateTime.Now}"); //这里可以写你的定时要执行的任务。。 } catch (Exception ex) { Console.WriteLine(ex.Message); } } else //表示: 因 waitObject被 Set()触发 { Console.WriteLine("手动触发(按需处理)"); } }, null, TimeSpan.FromSeconds(5), //表示:超时时间(毫秒) 即:多少毫秒执行一次回调(第二个参数WaitOrTimerCallback callBack) false // true表示:仅执行一次回调(单次触发),false表示:周期性触发回调(每次超时或事件触发后,自动重置等待) ); Console.WriteLine("定时任务已启动,按任意键停止..."); Console.ReadKey(); // 程序退出前调用:释放资源 rwh.Unregister(waitHandle); waitHandle.Dispose(); Console.WriteLine("任务已停止"); }
3、异常处理
- 案例
ThreadPool.QueueUserWorkItem(_ => { try { /* 代码 */ } catch (Exception ex) { /* 记录日志 */ } });
4、如何合理设置线程池的最小 / 最大线程数?
-
CPU 密集型任务(如数据计算、图像处理): 线程数过多会导致频繁上下文切换,反而降低效率。
-
I/O 密集型任务(如数据库查询、网络请求): 线程大部分时间在等待 I/O,可适当增加线程数。
场景类型 最小线程数建议 最大线程数建议 说明 CPU 密集型 等于 CPU 逻辑核心数(如 8 核设为 8) 逻辑核心数 × 1~2(如 8 核设为 8~16) 避免线程过多导致上下文切换开销 I/O 密集型 逻辑核心数 × 2~4(如 8 核设为 16~32) 根据并发需求(如 100~500,需测试) 线程大部分时间等待 I/O,可适当增加 混合场景 逻辑核心数 × 1~2 逻辑核心数 × 10~20(如 8 核设为 80~160) 平衡 CPU 和 I/O 需求,需通过压测优化
5、配置线程池最大最小数量
1、代码配置
- 使用
ThreadPool.SetMinThreads
和ThreadPool.SetMaxThreads
方法调整,示例如下using System; using System.Threading; class ThreadPoolConfigExample { static void Main() { // 获取当前线程池配置 ThreadPool.GetMinThreads(out int minWorker, out int minIO); ThreadPool.GetMaxThreads(out int maxWorker, out int maxIO); Console.WriteLine($"默认最小工作线程: {minWorker}, 最小I/O线程: {minIO}"); Console.WriteLine($"默认最大工作线程: {maxWorker}, 最大I/O线程: {maxIO}"); //1、最小工作线程是线程池为处理 CPU 密集型任务(如数值计算、逻辑处理)保留的最小线程数。 // 当新任务到达时,线程池会优先创建线程直至达到此最小值,之后才切换到动态管理算法 //2、最小 I/O 线程是线程池为处理 异步 I/O 操作(如文件读写、网络请求)保留的最小线程数。 // 这些线程专门用于接收操作系统完成的 I/O 事件通知,而非执行计算逻辑 // 调整线程池配置(根据任务类型) bool minSet = ThreadPool.SetMinThreads(8, 8); // 最小工作线程=8,最小I/O线程=8 bool maxSet = ThreadPool.SetMaxThreads(32, 100); // 最大工作线程=32,最大I/O线程=100 if (minSet && maxSet) { Console.WriteLine("线程池配置调整成功"); } else { Console.WriteLine("线程池配置调整失败(值可能超出系统限制)"); } } }
2、配置文件方式(.NET 6+)
- 在 .runtimeconfig.json 或项目文件中配置:
{ "runtimeOptions": { "configProperties": { "System.Threading.ThreadPool.MinThreads": 4, "System.Threading.ThreadPool.MaxThreads": 16 } } }
6、为什么线程池不适合长时间任务?
线程池的设计目标是:
- 高效复用线程
- 快速处理大量短时任务
- 自动管理线程数量(创建、销毁、回收)
如果你提交一个长时间任务到线程池:
- 该线程被“占用”,无法处理其他任务
- 如果大量此类任务提交,线程池可能耗尽线程
- 线程池需要时间“探测”是否需要创建新线程(有延迟)
7、注意事项
- 避免过度调整: .NET Core 3.0+ 引入了 “自适应线程池”,会根据任务负载动态调整线程数,大多数情况下无需手动设置。
- 测试验证: 调整后需通过压测验证性能(如监控 CPU 利用率、任务排队时间),避免盲目增大线程数导致资源耗尽。
- 最小线程数的作用: 仅控制线程池 “初始预热” 的线程数,实际运行时线程池会根据任务量动态创建更多线程(不超过最大限制)。
- 异常处理: SetMinThreads 和 SetMaxThreads 可能返回false(如设置值超出系统限制),需判断返回值。
四、Task
1、定义与优势
Task
表示一个异步操作,封装了耗时任务(如 I/O 操作、计算任务),避免阻塞主线程。相比传统线程Thread
,Task
更轻量(基于线程池管理),支持状态监控
、取消机制
、异常处理
和任务组合
,显著提升资源利用率和代码可维护性
2、生命周期与状态
Task.Status
属性跟踪任务状态,包括:
-
Created
:已创建未启动 -
WaitingForActivation
:等待被调度执行 -
WaitingToRun
:已被调度但尚未开始执行 -
Running
:正在执行 -
RanToCompletion
:成功完成 -
Faulted
:异常失败 -
Canceled
:已取消static async Task Main() { var task = new Task(() => Thread.Sleep(500)); //显式创建但未启动,task.Status = Created task.Start(); //WaitingToRun: 启动任务后,task.Status = WaitingToRun Thread.Sleep(100); // 确保任务进入执行状态 task.Status = Running await task; //正常完成 task.Status = RanToCompletion //异常导致失败 task.Status = Faulted var faultedTask = Task.Run(() => throw new Exception()); try { await faultedTask; } catch {} Console.WriteLine($"异常: {faultedTask.Status}"); //显式取消 task.Status = Canceled var cts = new CancellationTokenSource(); var canceledTask = Task.Run(() => { while (true) cts.Token.ThrowIfCancellationRequested(); }, cts.Token); cts.Cancel(); try { await canceledTask; } catch {} Console.WriteLine($"取消: {canceledTask.Status}"); // Canceled }
3、创建与启动
1、Task.Start (手动控制的方式)
- 手动启动: 需先通过构造函数
new Task()
创建任务(此时任务状态为TaskStatus.Created
),再显式调用.Start()
启动任务。 - 延迟控制: 允许在任务创建后、启动前配置参数(如
CancellationToken
、TaskCreationOptions
),适合需要条件触发的场景 - 灵活调度: 默认使用线程池,但可通过参数指定自定义 TaskScheduler(如
TaskScheduler.FromCurrentSynchronizationContext()
在 UI 线程执行) - 独立线程支持: 结合
TaskCreationOptions.LongRunning
可创建独立线程(非线程池),避免长时间任务导致线程池饥饿 - 适用场景: 仅在需要 延迟启动、独立线程或自定义调度 时使用
Task task = new Task(() => DoWork()); // 创建后任务未启动 task.Start(); // 手动启动 // 使用 Task.Start(需独立线程的长时任务) var longTask = new Task(() => ProcessData(), TaskCreationOptions.LongRunning); longTask.Start(); await longTask;
2、Task.Run(最简洁的方式)
-
自动启动: 静态方法,创建任务的同时立即启动,无需额外调用 .Start(),简化代码流程
-
立即执行: 任务在调用后直接进入
WaitingToRun
或Running
状态,由线程池调度执行 -
强制线程池: 始终使用
TaskScheduler.Default
(线程池调度器),无法脱离线程池或指定独立线程 -
适用场景: 优先选择 Task.Run()以简化代码并减少错误。日常开发中的大多数后台任务,如简单的计算、非阻塞的 I/O 操作等。
-
无法精细控制: 如不能指定 LongRunning或自定义调度器,不适合长时间阻塞操作(可能耗尽线程池资源)
Task task = Task.Run(() => DoWork()); // 创建并立即启动 // 使用 Task.Run(推荐多数场景) var result = await Task.Run(async () => await FetchDataAsync()); // 这个 Task 是“立即完成”的,本质上是同步的 Task<int> syncTask = Task.FromResult(42);
-
伪异步: Task.Run允许将同步代码调度到线程池线程执行,通过Task.Run包装的同步代码并非真正的异步,只是将阻塞操作转移到后台线程,仍会占用线程资源。若大量使用Task.Run包装同步I/O操作(如文件读写、网络请求),会导致线程池线程被阻塞等待I/O完成,进而耗尽线程池资源,影响其他任务的执行
例如:// 伪异步:实际仍阻塞线程池线程 var response = await Task.Run(() => File.ReadAllText("largefile.txt")); //真异步:使用真正的异步I/O var response = await File.ReadAllTextAsync("largefile.txt"); // 真异步:等待网络请求时不占用线程 var response = await httpClient.GetAsync("https://api.example.com");
3、Task.Factory.StartNew(更灵活的方式)
-
Task.Factory.StartNew提供了比 Task.Run更丰富的配置选项,允许你对任务的创建和调度进行更精细的控制
// 基本用法,与 Task.Run 类似 Task task = Task.Factory.StartNew(() => { Console.WriteLine("通过 TaskFactory 启动的任务"); }); // 提供配置选项,例如指定为长时间运行的任务 // 提示:线程池会优化短时任务。对于长时间运行的任务,指定 LongRunning 选项可能更好。 Task longRunningTask = Task.Factory.StartNew(() => { Console.WriteLine("这是一个长时间运行的任务"); }, TaskCreationOptions.LongRunning); // 此选项会提示调度器可能更适合使用独立线程 // 传递状态对象,并可访问 Task.AsyncState 属性 var stateData = new { Name = "MyData" }; Task taskWithState = Task.Factory.StartNew((stateObj) => { Console.WriteLine($"收到的状态数据: {stateObj}"); }, stateData); Console.WriteLine(taskWithState.AsyncState); // 输出: { Name = MyData }
特点与适用场景:
高度可配置: 可以通过 TaskCreationOptions(如 LongRunning, PreferFairness)和 TaskScheduler来控制任务行为状态传递: 可以向任务传递额外的状态数据,并通过 AsyncState属性检索
推荐场景: 需要自定义任务行为(如长时间运行的任务)、需要控制任务调度策略或需要使用特定计划程序的情况
4、Task.Result 和Task.Wait()
Task.Result
:- 在.NET中,
Task.Result
属性用于获取异步任务的结果值,但它会阻塞当前线程直到任务完成,包括成功、取消或失败)。这意味着在任务完成前,当前线程无法执行其他操作Task<int> task = Task.Run(() => 42); int result = task.Result; // 阻塞直到任务完成,返回42
Task.Wait()
:
在 .NET 中,Task.Wait()
是一个用于同步阻塞当前线程,直到Task
完成执行的方法。它与Task.Result
非常相似,与Task.Result
不同的是Wait()
本身不返回 Task 的结果值(因为它返回 void)static void Main() { Console.WriteLine("1️⃣ 主线程准备启动任务…"); // 启动一个耗时 2 秒的任务 Task task = Task.Delay(2000); Console.WriteLine("2️⃣ 任务已启动,还没完成。"); // 🔒 关键点:主线程被 Wait() 卡住 2 秒 task.Wait(); Console.WriteLine("3️⃣ 任务完成,主线程继续。"); }
- 比较
维度 await task task.Result task.Wait() 线程行为 ✅非阻塞(等待时释放当前线程) ❌阻塞当前线程 ❌阻塞当前线程 结果获取 ✅自动解包 Task,直接返回 T ❌手动获取,本身是属性 ❌无返回值,需通过 Task.Result获取 异常处理 ✅直接抛出原始异常 ❌封装为 AggregateException ❌封装为 AggregateException 死锁风险 ✅无 ❌高(尤其在UI/ASP.NET上下文) ❌高(尤其在UI/ASP.NET上下文) 推荐程度 ✅ 首选 ❌ 尽量避免 ❌ 尽量避免 适用场景 ✅所有异步编程场景 ❌极少数需要强制同步的场景 ❌极少数需要强制同步的场景
5、Task.FromResult():自动包装返回值
Task.FromResult
是.NET框架中用于创建已完成任务
的方法,返回一个状态为RanToCompletion
且包含指定结果的Task<TResult>
对象- 若函数返回非 Task 值(如 return 100),编译器会自动包装微
return Task.FromResult(100)
- 当调用
Task.FromResult(100)
时,编译器根据整数值100
推断TResult
为int
,因此返回类型为Task<int>
Task<int> task = Task.Run(() => { Thread.Sleep(1000); // 模拟耗时的 CPU 密集型工作 return 100; //编译器会自动将return 100 包装成 return Task.FromResult(100),其返回类型为 Task<int>; });
6、Task.ContinueWith():延续任务
- 在 .NET 的异步编程中,Task的延续操作 (ContinueWith) 是一项强大功能,它允许你在一个任务(称为“前置任务”)完成后自动执行后续操作。这非常适合用于创建任务链、处理异步结果、异常处理或资源清理等场景
- 基本延续
最基本的用法是在一个任务完成后执行一段代码。延续方法通过 ContinueWith接收前置任务作为参数,你可以访问前置任务的状态和结果//创建一个任务(前置任务) Task<string> firstTask = Task.Run(() => { // 模拟一些工作 return "Hello from Task"; }); //创建前置任务的延续任务 Task continuationTask = firstTask.ContinueWith(antecedent => { // antecedent 就是 firstTask string result = antecedent.Result; // 获取前一个任务的结果 Console.WriteLine(result); // 输出 "Hello from Task" }); continuationTask.Wait(); // 等待延续任务完成
- 异常处理
延续任务可以检查前置任务是否发生故障(IsFaulted),并处理其异常。这对于集中错误处理逻辑非常有用//创建执行一个任务(前置任务)模拟任务抛出异常 Task faultedTask = Task.Run(() => { throw new InvalidOperationException("出错啦"); }); //创建前置任务的延续任务 Task exceptionHandler = faultedTask.ContinueWith(antecedent => { //antecedent.IsFaulted = true 表示存在未处理的异常 if (antecedent.IsFaulted) // 检查前置任务是否因异常而失败。即:检查faultedTask 这个任务是否异常而失败 { // 展平异常树并获取内部异常 Console.WriteLine(antecedent.Exception?.Flatten().InnerException.Message); } }, TaskContinuationOptions.OnlyOnFaulted); // 此选项表示仅在故障时执行 exceptionHandler.Wait();
7、Task.WhenAll
-
Task.WhenAll: 等待所有的任务都完成(无论成功、失败还是被取消),这个新任务才会完成。
-
✅ 使用案例:并发获取多个 API 数据
using System; using System.Net.Http; using System.Threading.Tasks; public class ApiService { private static readonly HttpClient _httpClient = new HttpClient(); public async Task<UserInfo> GetUserAsync(int userId) { var response = await _httpClient.GetStringAsync($"https://api.example.com/users/{userId}"); return JsonConvert.DeserializeObject<UserInfo>(response); } public async Task<OrderInfo> GetOrderAsync(int orderId) { var response = await _httpClient.GetStringAsync($"https://api.example.com/orders/{orderId}"); return JsonConvert.DeserializeObject<OrderInfo>(response); } public async Task<ProductInfo> GetProductAsync(int productId) { var response = await _httpClient.GetStringAsync($"https://api.example.com/products/{productId}"); return JsonConvert.DeserializeObject<ProductInfo>(response); } // 使用 Task.WhenAll 并发获取所有数据 public async Task<UserData> GetUserDataAsync(int userId, int orderId, int productId) { // 并发启动三个异步请求 var userTask = GetUserAsync(userId); var orderTask = GetOrderAsync(orderId); var productTask = GetProductAsync(productId); // 等待所有任务完成(参数也可以接受数组) await Task.WhenAll(userTask, orderTask, productTask); // 获取结果(此时所有任务都已完成) var user = await userTask; var order = await orderTask; var product = await productTask; return new UserData { User = user, Order = order, Product = product }; } }
8、Task.WhenAny
-
Task.WhenAny: 它本身只关心“完成”这个状态,不区分成功还是失败。即使有任务失败,只要有一个任务完成(无论是成功完成还是失败),WhenAny就会返回
-
✅ 使用案例:从多个镜像源获取数据(选择最快响应)
public async Task<string> FetchFromFastestSourceAsync(List<string> urls) { // 为每个 URL 创建一个获取任务 var tasks = urls.Select(async url => { try { return await _httpClient.GetStringAsync(url); } catch { // 如果某个源失败,返回 null 或抛出异常 return null; } }).ToList(); // 等待第一个成功返回的任务 var completedTask = await Task.WhenAny(tasks); // 获取第一个完成的任务的结果 string result = await completedTask; // 可以取消其他仍在运行的任务(可选,需支持 CancellationToken) // ... if (result == null) throw new Exception("All sources failed."); return result; }
9、Task.ConfigureAwait
1、简介
-
ConfigureAwait 是 .NET 中 Task 和 Task 类的一个重要方法,主要用于控制 await 表达式之后的延续(continuation) 代码在哪个上下文中执行。
它的核心作用是解决死锁问题和性能问题,尤其是在编写库代码时至关重要。
2、核心作用
-
当你
await
一个Task
时,await
之后的代码(即延续代码)默认会尝试捕获当前的SynchronizationContext
或TaskScheduler
,并在Task
完成后,将延续代码调度回这个捕获的上下文中执行。ConfigureAwait 允许你改变这个默认行为。
3、ConfigureAwait(true) (默认行为)
- 行为: await 后的延续代码会尝试在捕获的上下文中执行。
- 何时发生: 如果当前线程有 SynchronizationContext(如 UI 线程的 WindowsFormsSynchronizationContext, DispatcherSynchronizationContext 或 ASP.NET 的 AspNetSynchronizationContext),它会被捕获。否则,捕获当前的 TaskScheduler。
- 目的: 确保在 UI 应用中,更新 UI 控件的代码在 UI 线程上执行;在 ASP.NET 中,确保访问 HttpContext 等特定于请求的资源。
- 风险: 如果在同步上下文中调用 Wait() 或 .Result,并且 await 的 Task 需要回到该上下文执行延续,但该上下文被阻塞,就会导致死锁。
4、ConfigureAwait(false)
- 行为: await 后的延续代码不会尝试回到捕获的上下文。它将在 Task 完成时所在线程(通常是线程池线程)上直接执行。
- 目的:
- 避免死锁: 这是使用 ConfigureAwait(false) 最主要的原因,尤其是在库代码中。
- 提高性能: 省去了调度回原始上下文的开销,对于不需要特定上下文的操作(如纯计算、日志记录、数据处理等),性能更优。
- 限制: 不能在 ConfigureAwait(false) 的延续代码中直接访问需要特定上下文的资源(如 UI 控件、ASP.NET 的 HttpContext.Current)。
5、场景:UI 应用中的死锁风险
- 案例代码
// 假设这是在 WinForms 或 WPF 的按钮点击事件中 private void Button_Click(object sender, EventArgs e) { // ❌ 危险!在 UI 线程上同步阻塞 DoSomethingAsync().Wait(); // 或 .Result } private async Task DoSomethingAsync() { // 1. 捕获 UI 线程的 SynchronizationContext await Task.Delay(1000); // 模拟异步操作 // 2. 延续代码需要回到 UI 线程执行 UpdateUI(); // 更新 UI 控件 }
- 问题:
1、Button_Click 在 UI 线程调用 Wait(),阻塞了 UI 线程。
2、DoSomethingAsync() 中的 await Task.Delay(1000) 会捕获 UI 上下文。
3、当 Task.Delay 完成后,await 后的代码(UpdateUI())需要回到 UI 线程执行。
4、但 UI 线程正被 Wait() 阻塞,无法执行新的工作,导致死锁。 - ✅ 解决方案 1 (应用层): 使用 async/await 避免阻塞(推荐)
优点:// ✅ 正确方式:将事件处理程序改为 async private async void Button_Click(object sender, EventArgs e) { // await 会立即返回控制权给调用者(UI 框架),不会阻塞 UI 线程 await DoSomethingAsync(); // 当 DoSomethingAsync 完成后,UpdateUI() 会自动在 UI 线程上执行 // 因为 await 捕获了 UI 上下文 UpdateUI(); // 安全地更新 UI } private async Task DoSomethingAsync() { // 模拟耗时的异步操作 (如网络请求、文件读写) await Task.Delay(1000); // 这不会阻塞 UI // 注意:这里不需要 UpdateUI(),它移到了 Button_Click 中 // ... 可以在这里进行一些后台数据处理 ... }
1、彻底避免死锁。
2、UI 保持响应:在 await Task.Delay(1000) 执行期间,UI 线程是自由的,可以响应其他用户操作。
3、代码清晰:逻辑顺序与阅读顺序一致。 - ✅ 解决方案 2:使用 ConfigureAwait(false) + 回到 UI 上下文
如果由于某些原因(比如 DoSomethingAsync 是一个通用库方法,不应依赖 UI 上下文),你不想让 DoSomethingAsync 捕获 UI 上下文,但最终仍需要更新 UI,可以这样做:
优点:// ✅ 正确方式:避免在库方法中捕获上下文 private async void Button_Click(object sender, EventArgs e) { // 先在不捕获上下文的情况下完成后台工作 await DoSomethingAsyncWithoutUI().ConfigureAwait(false); // 明确地回到 UI 上下文来更新 UI // 因为 Button_Click 是 async void,await 会自动捕获 UI 上下文 UpdateUI(); } // 这个方法现在是“上下文无关”的,更适合库代码 private async Task DoSomethingAsyncWithoutUI() { // 使用 ConfigureAwait(false) 避免捕获调用者的上下文 await Task.Delay(1000).ConfigureAwait(false); // ... 执行不需要 UI 上下文的后台逻辑 ... // 例如:数据处理、日志记录等 }
1、避免了上下文捕获的开销。
2、DoSomethingAsyncWithoutUI 方法更通用,可以在任何地方安全调用。
3、仍然避免了死锁。
6、NET 8 及更高版本
-
.NET 8 引入了 ConfigureAwaitOptions枚举,提供了更丰富的控制选项
ConfigureAwaitOptions.None: 等效于 ConfigureAwait(false)。
ConfigureAwaitOptions.ContinueOnCapturedContext: 等效于 ConfigureAwait(true)。
ConfigureAwaitOptions.SuppressThrowing: 等待任务但不抛出异常(仅适用于 Task,不适用于 Task)。
ConfigureAwaitOptions.ForceYielding: 强制异步延续,即使任务已完成,也总是 yield 到线程池// .NET 8 中的新选项用法 await someTask.ConfigureAwait(ConfigureAwaitOptions.None);
五、Async Await
1. async 关键字的作用
- 标记异步方法:
async
用于修饰方法,表明该方法是一个异步方法,内部可能包含await
关键字。 - 启用 await 支持: 只有标记为
async
的方法中,才能使用await
关键字(否则会编译错误)。 - 返回类型约束: 异步方法的返回类型只能是
Task
、Task<T>
、ValueTask
、ValueTask<T>
等类型。void(仅用于事件处理器,不推荐普通使用) - 自动封装结果: 编译器会将异步方法的返回值自动封装到 Task 中,无需手动手动创建任务对象。
rerurn 50; // 编译器会利用Task.FromResult(value)或类似机制帮你完成包装成Task<int>;
2. await 关键字的作用
- 暂停当前方法执行: 当
await
遇到异步操作(如Task
或Task<T>
)时,会暂停当前异步方法的执行,直到异步操作完成,期间释放线程资源(不阻塞调用线程) - 释放线程资源: 在等待期间,当前线程会被释放(返回给线程池),去处理其他任务,实现非阻塞操作,提高程序吞吐量。
- 恢复方法执行: 当被等待的异步操作完成后,await 会自动将后续代码调度到合适的线程(如 UI 线程)继续执行,无需手动处理回调。
- 简化异常处理: 可以像同步代码一样使用 try/catch 捕获异步操作中的异常,无需在回调中处理。
3. 异步操作的执行者
异步操作的实际执行由 外部系统
或线程池
完成,取决于任务类型:
- I/O 密集型任务(如:文件读写、网络请求(HTTP、数据库)、远程 API 调用):
- 这些操作的核心工作由
操作系统内核或硬件(如网卡、磁盘控制器)
完成,.NET 仅负责发起请求和等待结果,期间不占用任何 .NET 线程(包括线程池线程) - 不占用线程:发起请求后,当前线程立即释放;操作完成后,内核通知 .NET 线程池触发回调
- 这些操作的核心工作由
- CPU 密集型任务(如复杂计算):
-
通过 Task.Run()提交到 线程池线程 执行
-
完成后由线程池通知状态机恢复
// 示例 1:I/O 密集型(文件读取) async Task ReadFileAsync() { // 发起文件读取请求:由操作系统内核和底层硬件驱动处理 // 应用程序线程仅负责发起请求和回调处理(不占用线程池线程) string content = await File.ReadAllTextAsync("data.txt"); // 读取完成后,回调逻辑可能使用线程池线程 Console.WriteLine(content); } // 示例 2:CPU 密集型(计算任务) async Task CalculateAsync() { // 计算逻辑由线程池线程执行 int result = await Task.Run(() => { int sum = 0; for (int i = 0; i < 1_000_000; i++) sum += i; return sum; }); Console.WriteLine(result); }
-
4、.Net Async 状态机机制:实现异步的原理
1、简介
async/await
是.NET 简化异步编程的语法糖,其底层依赖编译器自动生成的异步状态机来管理异步操作的执行流程。核心目的是:在等待异步操作(如网络请求、IO 操作)时,释放当前线程,待操作完成后再从暂停处继续执行,实现 “非阻塞” 效果。
2、基本原理
- 当编译器遇到
async
方法时,会将其重写为一个实现了IAsyncStateMachine接口
的类(状态机类),这个类会:- 保存方法的局部变量、参数和执行状态(如
初始状态
,等待中
,已完成
等) - 通过
MoveNext
方法驱动状态转换,协调异步操作的执行
- 保存方法的局部变量、参数和执行状态(如
3、核心组成
-
IAsyncStateMachine接口
: 定义状态机的核心行为,包含两个方法:MoveNext()
:驱动状态机执行下一个状态(关键方法,负责状态转换和逻辑执行)SetStateMachine(IAsyncStateMachine stateMachine):
用于关联状态机实例(通常由编译器自动调用)
-
状态机类负责: (状态机类是编译器生成的类,名称通常为
<<MethodName>>d__0
):- 保存上下文: 在 await处暂停时,记录当前局部变量、执行位置(状态码state)等信息
初始状态:state = -1(表示方法尚未开始执行)
最终完成状态:state = -2(表示方法完全执行完毕) - 注册回调: 为异步操作(如 Task)注册一个延续(continuation),即操作完成后需执行的代码块
- 恢复执行: 当异步操作完成时,状态机从保存的位置继续执行后续代码
- 保存上下文: 在 await处暂停时,记录当前局部变量、执行位置(状态码state)等信息
4、执行流程
-
以一个简单的 async 方法为例
public async Task<string> FetchDataAsync(string url) { var client = new HttpClient(); // 第一个await点 string result = await client.GetStringAsync(url); // 第二个await点(假设) await SaveToFileAsync(result); // 第三个await点(假设) await GetDateByIdAsync(1); // 第四个await点(假设) await DeleteByIdAsync(1); return result; }
状态值完整变化轨迹
-1(初始)→ 0(等待第一个await)→ 1(等待第二个await)→ 2(等待第三个await)→ 3(等待第四个await)→ -2(执行完毕)
详细执行流程与state值变化
- 初始调用:state = -1
- 状态机初始化:创建局部变量(client、result)和参数(url)的副本
- 执行同步代码至第一个await:
var client = new HttpClient(); string result = await client.GetStringAsync(url); // 第一个await
- 状态机操作:
- 调用client.GetStringAsync(url)获取异步任务(task1)
- 若task1未完成:将state设为0,注册task1的完成回调(触发MoveNext()),释放当前线程
- 第一个任务完成:state = 0
- task1(GetStringAsync)完成后,回调触发MoveNext(),此时state = 0
- 恢复执行第一个await之后的代码,直至第二个await:
result = task1.Result; // 获取第一个任务结果 await SaveToFileAsync(result); // 第二个await
- 状态机操作:
- 调用SaveToFileAsync(result)获取异步任务(task2)
- 若task2未完成:将state设为1,注册task2的完成回调,释放线程
- 第二个任务完成:state = 1
- task2(SaveToFileAsync)完成后,回调触发MoveNext(),此时state = 1
- 恢复执行第二个await之后的代码,直至第三个await:
// 从第二个await恢复 await GetDateByIdAsync(1); // 第三个await
- 状态机操作:
- 调用GetDateByIdAsync(1)获取异步任务(task3)
- 若task3未完成:将state设为2,注册task3的完成回调,释放线程
- 第三个任务完成:state = 2
- task3(GetDateByIdAsync)完成后,回调触发MoveNext(),此时state = 2
- 恢复执行第三个await之后的代码,直至第四个await:
// 从第三个await恢复 await DeleteByIdAsync(1); // 第四个await
- 状态机操作:
- 调用DeleteByIdAsync(1)获取异步任务(task4)
- 若task4未完成:将state设为3,注册task4的完成回调,释放线程
- 第四个任务完成:state = 3
- task4(DeleteByIdAsync)完成后,回调触发MoveNext(),此时state = 3
- 执行剩余代码直至方法结束:
// 从第四个await恢复 return result; // 返回结果
- 状态机操作:
- 设置返回结果到最终的Task中
- 将state设为-2(标记方法执行完毕),后续不再处理
- 初始调用:state = -1
更多推荐
所有评论(0)