.NET基础拾遗(5)多线程开发基础
2026/7/26 22:00:28 网站建设 项目流程

.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($"工作线程: 第 {i+1} 次循环"); // 模拟耗时操作,让出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):确保同一时间只有一个线程能执行花括号内的代码。- 锁对象选择:必须是所有相关线程都能访问的引用类型,且不要用thistypeof或字符串字面量,因为这些可能被其他代码意外锁定。- 性能影响:锁会引入开销,因此只保护必要的临界区。### 四、高级抽象:Task与async/await从.NET 4.0开始,System.Threading.Tasks.Task提供了更高级的并行编程模型。它基于线程池,自动管理线程的创建和复用。配合async/await(C# 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 运行中... 迭代 {i+1}"); Task.Delay(300).Wait(); // 模拟异步等待 } }); // 创建返回值的Task Task<int> 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} 字符"); // 并发下载多个页面 Task<string> task1 = DownloadWebPageAsync("https://httpbin.org/get"); Task<string> 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 Task<string> 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 Task(C# 7.1+),但控制台应用仍需要ReadLine保持进程。### 五、总结多线程开发是.NET中提升性能和响应性的核心技能。从最基础的Thread类,到线程同步的lock,再到现代的Taskasync/await,我们经历了一条从底层到高层的演进路径:1.Thread类:提供最直接的控制,但手动管理线程生命周期和同步容易出错。适合需要精细控制线程优先级的场景。2.lock同步:解决竞态条件,但过度使用会降低并发性能。应只保护最小的临界区。3.Task与async/await:基于线程池,自动管理线程复用;async/await让异步代码逻辑清晰,避免回调地狱。推荐用于大多数现代.NET应用。在实际开发中,建议优先使用Taskasync/await,仅在需要低级别控制时使用Thread。同时,牢记线程安全原则:共享可变资源必须同步,而不可变数据或线程本地存储则不需要。掌握这些基础,你就能构建出高效、稳定的多线程应用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询