C# 多线程知识点总结整理
2026/9/6 3:32:11 网站建设 项目流程

1. 引言

多线程是 C# 开发中绕不开的核心主题。无论是提升界面响应速度、并行处理大量数据,还是构建高吞吐的服务端程序,都离不开对线程、任务和异步模型的理解。本文从基础概念出发,系统梳理 C# 中多线程相关的核心知识点,帮助读者建立清晰的知识框架。

2. 基础概念

在深入代码之前,先厘清几个容易混淆的基础概念。

  • 进程:操作系统分配资源的基本单位,拥有独立的地址空间。
  • 线程:进程内的执行单元,同一进程内的线程共享内存和资源。
  • 线程池:由运行时维护的一组工作线程,用于复用线程、减少创建和销毁的开销。
  • 任务(Task):表示一个异步操作,是对线程的更高层抽象,默认在线程池上调度。
  • 异步与多线程的区别:异步关注的是不阻塞调用线程,多线程关注的是并行执行;异步不一定创建新线程,多线程也不一定异步。

3. 线程的创建与生命周期

最传统的方式是直接使用System.Threading.Thread类创建线程。

Thread thread = new Thread(() => { Console.WriteLine("子线程开始执行"); Thread.Sleep(1000); Console.WriteLine("子线程结束"); }); thread.Start(); thread.Join(); // 等待子线程结束 Console.WriteLine("主线程继续");

线程的常见状态包括:未启动(Unstarted)、运行中(Running)、阻塞(WaitSleepJoin)、终止(Stopped)等。可以通过ThreadState属性查看,但实际开发中更推荐使用任务和异步模型来管理并发。

4. 线程池与 Task

直接创建线程开销较大,因此 .NET 提供了线程池(ThreadPool)。线程池中的线程会被复用,适合执行大量短小的任务。

ThreadPool.QueueUserWorkItem(state => { Console.WriteLine("线程池任务执行中"); });

更现代的方式是使用Task,它封装了线程池调度,并支持返回值、异常处理和组合操作。

Task<int> task = Task.Run(() => { int sum = 0; for (int i = 1; i <= 100; i++) sum += i; return sum; }); int result = task.Result; // 阻塞等待结果 Console.WriteLine($"计算结果:{result}");

5. async / await 异步编程

asyncawait是 C# 5.0 引入的异步编程模型,让异步代码写起来像同步代码一样直观。它不会阻塞调用线程,而是通过状态机在等待期间释放线程。

async Task<string> DownloadAsync(string url) { using var client = new HttpClient(); string content = await client.GetStringAsync(url); return content; } // 调用方式 string html = await DownloadAsync("https://example.com"); Console.WriteLine(html.Length);

使用async/await时需要注意:async方法中应使用await而不是.Result.Wait(),否则可能造成死锁;UI 线程上的异步方法默认会捕获同步上下文,服务端场景则建议使用ConfigureAwait(false)避免不必要的上下文切换。

6. 线程同步与锁

多线程访问共享资源时,必须保证线程安全。C# 提供了多种同步机制。

6.1 lock 语句

lock是最常用的互斥锁,本质上是Monitor的语法糖。

private readonly object _lockObj = new object(); private int _counter; public void Increment() { lock (_lockObj) { _counter++; } }

6.2 Monitor 与 Mutex

Monitor提供了更细粒度的控制,如TryEnter支持超时;Mutex可用于跨进程同步。

if (Monitor.TryEnter(_lockObj, TimeSpan.FromSeconds(1))) { try { _counter++; } finally { Monitor.Exit(_lockObj); } }

6.3 信号量 Semaphore / SemaphoreSlim

信号量用于限制同时访问某资源的线程数量,适合控制并发度。

using var semaphore = new SemaphoreSlim(2, 2); // 最多允许 2 个并发 await semaphore.WaitAsync(); try { // 访问受限资源 } finally { semaphore.Release(); }

6.4 读写锁 ReaderWriterLockSlim

当读操作远多于写操作时,读写锁可以提升并发性能:多个读线程可同时进入,写线程独占。

private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); private int _value; public int Read() { _rwLock.EnterReadLock(); try { return _value; } finally { _rwLock.ExitReadLock(); } } public void Write(int newValue) { _rwLock.EnterWriteLock(); try { _value = newValue; } finally { _rwLock.ExitWriteLock(); } }

7. 并发集合

在多线程环境下,普通的List<T>Dictionary<TKey, TValue>不是线程安全的。.NET 提供了专门的并发集合,位于System.Collections.Concurrent命名空间。

  • ConcurrentDictionary<TKey, TValue>:线程安全的字典,支持原子性的GetOrAddTryAdd等操作。
  • ConcurrentQueue<T>:线程安全的先进先出队列,适合生产者-消费者模型。
  • ConcurrentStack<T>:线程安全的后进先出栈。
  • ConcurrentBag<T>:无序集合,适合每个线程独立添加、偶尔取出的场景。
  • BlockingCollection<T>:在并发集合之上提供阻塞和边界控制,常用于生产者-消费者模式。
var queue = new ConcurrentQueue<int>(); queue.Enqueue(1); queue.Enqueue(2); if (queue.TryDequeue(out int item)) { Console.WriteLine($"取出:{item}"); }

8. 并行编程 Parallel

System.Threading.Tasks.Parallel类提供了并行循环的简化写法,适合 CPU 密集型任务。

Parallel.For(0, 100, i => { Console.WriteLine($"并行处理第 {i} 项"); }); var numbers = Enumerable.Range(1, 1000); Parallel.ForEach(numbers, n => { // 并行处理每个元素 });

使用Parallel时要注意:循环体内的操作应尽量独立,避免共享可变状态;如果操作涉及 I/O,更适合使用async/await而非并行循环。

8.1 实战:并行下载多个文件

下面通过一个实际场景演示如何用HttpClientTask.WhenAll实现多个文件的异步并发下载。相比Parallel.ForEach,这种方式在 I/O 密集型任务中能更高效地利用线程资源。

using System.Net.Http; async Task DownloadFilesAsync(string[] urls, string saveDir, CancellationToken token = default) { using var client = new HttpClient(); client.Timeout = TimeSpan.FromSeconds(30); var tasks = urls.Select(async url => { string fileName = Path.GetFileName(new Uri(url).LocalPath); string savePath = Path.Combine(saveDir, fileName); byte[] data = await client.GetByteArrayAsync(url, token); await File.WriteAllBytesAsync(savePath, data, token); Console.WriteLine($"已下载:{fileName}"); }); await Task.WhenAll(tasks); } // 调用示例 string[] urls = { "https://example.com/file1.zip", "https://example.com/file2.zip", "https://example.com/file3.zip" }; try { await DownloadFilesAsync(urls, @"C:\Downloads"); } catch (OperationCanceledException) { Console.WriteLine("下载已取消"); } catch (Exception ex) { Console.WriteLine($"下载失败:{ex.Message}"); }

这段代码的核心在于:每个下载任务都是独立的async方法,await会在等待网络响应时释放线程,因此Task.WhenAll可以同时发起多个请求,而不会像同步循环那样逐个阻塞等待。

8.1.1 与 Parallel.ForEach 的对比

Parallel.ForEach适合 CPU 密集型任务,它会把循环体分配到多个线程上并行执行;而文件下载属于 I/O 密集型任务,大部分时间都在等待网络响应。如果使用Parallel.ForEach下载文件,每个迭代都会占用一个线程池线程并阻塞等待,高并发下容易造成线程池饥饿。因此,I/O 密集型场景应优先选择async/await配合Task.WhenAll

8.1.2 异常处理与取消支持

当多个任务并发执行时,Task.WhenAll会等待所有任务完成,并把第一个抛出的异常作为AggregateException或直接抛出。上面的示例通过try/catch捕获了取消和普通异常。取消支持通过CancellationToken实现:调用方可以传入令牌,在需要时调用Cancel()中止所有下载任务,HttpClientFile.WriteAllBytesAsync都会响应取消请求并抛出OperationCanceledException

9. 线程安全与常见陷阱

多线程编程的难点往往不在 API 本身,而在于对共享状态的管理。以下是几个常见陷阱。

  • 竞态条件:多个线程同时读写同一变量,导致结果不确定。解决方式是加锁或使用原子操作(Interlocked)。
  • 死锁:两个线程各自持有一把锁,又互相等待对方释放。应避免嵌套锁,或使用统一的锁顺序。
  • 可见性问题:一个线程修改的变量,另一个线程可能看不到最新值。可使用volatile、锁或MemoryBarrier保证可见性。
  • 线程池饥饿:在async方法中同步阻塞(如.Result)会占用线程池线程,高并发下可能耗尽线程池。
  • 上下文切换开销:线程过多时,CPU 大量时间花在切换而非执行上,应合理控制并发度。
// 使用 Interlocked 实现原子自增,避免竞态条件 private int _counter; Interlocked.Increment(ref _counter);

10. 总结

C# 多线程知识体系可以概括为三条主线:一是线程与线程池等基础执行单元,二是Taskasync/await构成的现代异步模型,三是锁、信号量、并发集合等同步与协作工具。实际开发中,优先使用Taskasync/await,仅在需要精细控制或跨进程同步时才直接操作线程和Mutex。理解并发模型、掌握同步机制、规避常见陷阱,是写出健壮多线程程序的关键。

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

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

立即咨询