.NET基础拾遗(5)多线程开发基础

📅 2026/7/26 22:00:32
.NET基础拾遗(5)多线程开发基础
.NET基础拾遗5多线程开发基础一、多线程概述为什么需要它在.NET开发中多线程是提升应用程序性能和响应能力的关键技术。想象一下一个桌面应用程序在下载文件时如果使用单线程UI会冻结直到下载完成——用户无法操作界面体验极差。多线程允许程序同时执行多个任务比如一个线程处理用户输入另一个线程在后台下载数据第三个线程更新进度条。在.NET中线程是操作系统调度的最小执行单元。每个进程至少有一个主线程而多线程编程就是创建和管理额外的线程来并行工作。但要注意线程不是越多越好——线程切换有开销且访问共享资源时可能引发竞态条件。因此理解基础概念和实践至关重要。### 二、基础概念Thread类与线程生命周期.NET中最基础的多线程类是System.Threading.Thread。它允许你创建、启动、暂停、恢复和终止线程。线程的生命周期包括未启动、运行、阻塞、休眠、终止等状态。#### 2.1 创建和启动线程以下代码展示如何用Thread类创建并启动一个简单线程csharpusing System;using System.Threading;class Program{ static void Main() { // 创建线程对象指定要执行的方法 Thread workerThread new Thread(DoWork); // 设置为后台线程应用程序退出时自动终止 workerThread.IsBackground true; // 启动线程 workerThread.Start(); // 主线程继续执行其他任务 Console.WriteLine(主线程正在运行...); // 等待用户输入防止控制台退出 Console.ReadLine(); } // 线程要执行的方法 static void DoWork() { for (int i 0; i 5; i) { Console.WriteLine($工作线程: 第 {i1} 次循环); // 模拟耗时操作让出CPU给其他线程 Thread.Sleep(500); } Console.WriteLine(工作线程完成。); }}输出示例主线程正在运行...工作线程: 第 1 次循环工作线程: 第 2 次循环工作线程: 第 3 次循环工作线程: 第 4 次循环工作线程: 第 5 次循环工作线程完成。注释说明-Thread.Sleep(500)让当前线程暂停500毫秒模拟耗时工作。-IsBackground true后台线程不会阻止进程退出。如果设为前台线程即使主线程结束进程也会等待该线程完成。- 注意主线程的Console.ReadLine()保证了控制台不会立即关闭否则后台线程可能被强行终止。#### 2.2 线程参数传递与返回值线程方法默认不能直接返回值但可以通过回调或共享变量实现。以下示例展示如何传递参数csharpusing System;using System.Threading;class Program{ static void Main() { // 使用ParameterizedThreadStart传递对象参数 Thread worker new Thread(CalculateSum); worker.Start(10); // 传递一个整数自动装箱为object // 另一种方式使用Lambda表达式推荐 Thread worker2 new Thread(() { int result 0; for (int i 1; i 5; i) result i; Console.WriteLine($Lambda线程计算结果: {result}); }); worker2.Start(); Console.ReadLine(); } // 带参数的方法参数必须是object类型 static void CalculateSum(object maxObj) { int max (int)maxObj; // 拆箱 int sum 0; for (int i 1; i max; i) sum i; Console.WriteLine($工作线程: 1到{max}的和是{sum}); }}输出示例工作线程: 1到10的和是55Lambda线程计算结果: 15注释说明-ParameterizedThreadStart允许传递一个object参数。但需要注意拆箱的类型安全。- Lambda表达式更简洁且可以捕获外部变量注意闭包陷阱是现代.NET多线程编程的首选方式。### 三、线程同步避免竞态条件当多个线程访问共享资源如全局变量、文件、数据库连接时必须同步以防止数据不一致。最常用的同步机制是lock语句。#### 3.1 无同步的竞态条件演示先看一个反面教材csharpusing System;using System.Threading;class Program{ static int counter 0; // 共享资源 static void Main() { // 创建两个线程同时增加counter Thread t1 new Thread(IncrementCounter); Thread t2 new Thread(IncrementCounter); t1.Start(); t2.Start(); // 等待两个线程完成 t1.Join(); t2.Join(); Console.WriteLine($最终结果: {counter} (期望: 200000)); Console.ReadLine(); } static void IncrementCounter() { for (int i 0; i 100000; i) { // 非原子操作读取、加1、写入 counter; } }}输出示例每次运行可能不同最终结果: 156823 (期望: 200000)原因分析counter不是原子操作它包含“读取-修改-写入”三步。当两个线程同时读取到相同值比如100各自加1后写入结果只增加了1而不是2。这就是竞态条件。#### 3.2 使用lock同步修复方法用lock对象保护临界区csharpusing System;using System.Threading;class Program{ static int counter 0; // 锁对象必须是引用类型且通常设为private static readonly private static readonly object lockObj new object(); static void Main() { Thread t1 new Thread(IncrementCounter); Thread t2 new Thread(IncrementCounter); t1.Start(); t2.Start(); t1.Join(); t2.Join(); Console.WriteLine($最终结果: {counter} (期望: 200000)); Console.ReadLine(); } static void IncrementCounter() { for (int i 0; i 100000; i) { // 进入锁只允许一个线程执行此代码块 lock (lockObj) { counter; } // 离开锁其他线程可以进入 } }}输出结果稳定最终结果: 200000 (期望: 200000)注释说明-lock (lockObj)确保同一时间只有一个线程能执行花括号内的代码。- 锁对象选择必须是所有相关线程都能访问的引用类型且不要用this、typeof或字符串字面量因为这些可能被其他代码意外锁定。- 性能影响锁会引入开销因此只保护必要的临界区。### 四、高级抽象Task与async/await从.NET 4.0开始System.Threading.Tasks.Task提供了更高级的并行编程模型。它基于线程池自动管理线程的创建和复用。配合async/awaitC# 5.0可以写出类似同步代码的异步程序。#### 4.1 Task基础使用csharpusing System;using System.Threading.Tasks;class Program{ static void Main() { // 创建并启动一个Task Task task1 Task.Run(() { for (int i 0; i 3; i) { Console.WriteLine($Task1 运行中... 迭代 {i1}); Task.Delay(300).Wait(); // 模拟异步等待 } }); // 创建返回值的Task Taskint task2 Task.Run(() { int sum 0; for (int i 1; i 100; i) sum i; return sum; }); // 等待所有任务完成 Task.WaitAll(task1, task2); // 获取task2的结果会阻塞直到完成 Console.WriteLine($Task2 结果: {task2.Result}); // 使用ContinueWith链式操作 Task.Run(() Hello) .ContinueWith(prev Console.WriteLine(prev.Result World)) .Wait(); Console.ReadLine(); }}输出示例Task1 运行中... 迭代 1Task1 运行中... 迭代 2Task1 运行中... 迭代 3Task2 结果: 5050Hello World注释说明-Task.Run在线程池上执行异步操作推荐用于CPU密集型任务。-Task.Delay非阻塞的异步等待相比Thread.Sleep不会占用线程。-Task.WaitAll阻塞当前线程直到所有任务完成。-ContinueWith在任务完成后执行后续操作类似回调。#### 4.2 async/await异步编程async/await让异步代码更易读特别适合I/O密集型操作如文件读写、网络请求csharpusing System;using System.Net.Http;using System.Threading.Tasks;class Program{ static async Task Main(string[] args) // Main也可以async { Console.WriteLine(开始下载...); string content await DownloadWebPageAsync(https://example.com); Console.WriteLine($下载完成内容长度: {content.Length} 字符); // 并发下载多个页面 Taskstring task1 DownloadWebPageAsync(https://httpbin.org/get); Taskstring task2 DownloadWebPageAsync(https://httpbin.org/ip); // 等待所有任务完成不阻塞UI线程 string[] results await Task.WhenAll(task1, task2); Console.WriteLine($第一个结果长度: {results[0].Length}); Console.WriteLine($第二个结果长度: {results[1].Length}); Console.ReadLine(); } static async Taskstring DownloadWebPageAsync(string url) { using (HttpClient client new HttpClient()) { // await 会释放当前线程但不会阻塞调用者 string response await client.GetStringAsync(url); return response; } }}输出示例开始下载...下载完成内容长度: 1256 字符第一个结果长度: 312第二个结果长度: 45注释说明-async标记方法await等待异步操作完成。- 在await执行时当前线程会返回线程池不占用资源操作完成后从线程池取回线程继续执行。-Task.WhenAll并发等待多个任务提高效率。- 注意Main方法也可以标记为async TaskC# 7.1但控制台应用仍需要ReadLine保持进程。### 五、总结多线程开发是.NET中提升性能和响应性的核心技能。从最基础的Thread类到线程同步的lock再到现代的Task和async/await我们经历了一条从底层到高层的演进路径1.Thread类提供最直接的控制但手动管理线程生命周期和同步容易出错。适合需要精细控制线程优先级的场景。2.lock同步解决竞态条件但过度使用会降低并发性能。应只保护最小的临界区。3.Task与async/await基于线程池自动管理线程复用async/await让异步代码逻辑清晰避免回调地狱。推荐用于大多数现代.NET应用。在实际开发中建议优先使用Task和async/await仅在需要低级别控制时使用Thread。同时牢记线程安全原则共享可变资源必须同步而不可变数据或线程本地存储则不需要。掌握这些基础你就能构建出高效、稳定的多线程应用。