ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C# 多线程知识点总结整理

2026/9/6 3:32:21 拓冰建站 浏览量
C# 多线程知识点总结整理 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它封装了线程池调度并支持返回值、异常处理和组合操作。Taskint 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 异步编程async和await是 C# 5.0 引入的异步编程模型让异步代码写起来像同步代码一样直观。它不会阻塞调用线程而是通过状态机在等待期间释放线程。async Taskstring 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 与 MutexMonitor提供了更细粒度的控制如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. 并发集合在多线程环境下普通的ListT、DictionaryTKey, TValue不是线程安全的。.NET 提供了专门的并发集合位于System.Collections.Concurrent命名空间。ConcurrentDictionaryTKey, TValue线程安全的字典支持原子性的GetOrAdd、TryAdd等操作。ConcurrentQueueT线程安全的先进先出队列适合生产者-消费者模型。ConcurrentStackT线程安全的后进先出栈。ConcurrentBagT无序集合适合每个线程独立添加、偶尔取出的场景。BlockingCollectionT在并发集合之上提供阻塞和边界控制常用于生产者-消费者模式。var queue new ConcurrentQueueint(); queue.Enqueue(1); queue.Enqueue(2); if (queue.TryDequeue(out int item)) { Console.WriteLine($取出{item}); }8. 并行编程 ParallelSystem.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 实战并行下载多个文件下面通过一个实际场景演示如何用HttpClient和Task.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()中止所有下载任务HttpClient和File.WriteAllBytesAsync都会响应取消请求并抛出OperationCanceledException。9. 线程安全与常见陷阱多线程编程的难点往往不在 API 本身而在于对共享状态的管理。以下是几个常见陷阱。竞态条件多个线程同时读写同一变量导致结果不确定。解决方式是加锁或使用原子操作Interlocked。死锁两个线程各自持有一把锁又互相等待对方释放。应避免嵌套锁或使用统一的锁顺序。可见性问题一个线程修改的变量另一个线程可能看不到最新值。可使用volatile、锁或MemoryBarrier保证可见性。线程池饥饿在async方法中同步阻塞如.Result会占用线程池线程高并发下可能耗尽线程池。上下文切换开销线程过多时CPU 大量时间花在切换而非执行上应合理控制并发度。// 使用 Interlocked 实现原子自增避免竞态条件 private int _counter; Interlocked.Increment(ref _counter);10. 总结C# 多线程知识体系可以概括为三条主线一是线程与线程池等基础执行单元二是Task与async/await构成的现代异步模型三是锁、信号量、并发集合等同步与协作工具。实际开发中优先使用Task和async/await仅在需要精细控制或跨进程同步时才直接操作线程和Mutex。理解并发模型、掌握同步机制、规避常见陷阱是写出健壮多线程程序的关键。