【.Net技术栈梳理】04-核心框架与运行时(线程处理)

文章目录

  • 1. 线程管理
    • 1.1 线程的核心概念:System.Threading.Thread
    • 1.2 现代线程管理:System.Threading.Tasks.Task 和 Task Parallel Library (TPL)
    • 1.3 状态管理和异常处理
    • 1.4 协调任务:async/await 模式
  • 2. 线程间通信
    • 2.1 共享内存与竞态条件
    • 2.2 同步原语:确保线程安全
    • 2.3 线程安全的数据结构
  • 2. 多线程编程模式与最佳实践
    • 2.1 模式
    • 2.1 最佳实践与常见陷阱
    • 2.2 补充

1. 线程管理

.NET 的线程管理建立在操作系统线程之上,但提供了更高级别的抽象和更丰富的功能来简化并发编程。

1.1 线程的核心概念:System.Threading.Thread

这是最基础的线程类,直接包装了操作系统线程。

using System.Threading;// 创建并启动一个新线程
Thread newThread = new Thread(WorkerMethod);
newThread.Start(); // 开始执行 WorkerMethodvoid WorkerMethod()
{Console.WriteLine($"我在另一个线程上运行!线程ID: {Thread.CurrentThread.ManagedThreadId}");
}
  • 前台线程 vs. 后台线程:
    • 前台线程:默认创建的线程是前台线程。只要有一个前台线程还在运行,应用程序进程就不会终止。
    • 后台线程:通过 IsBackground = true 设置。当所有前台线程结束时,CLR 会强制终止所有后台线程,无论其是否执行完毕。适用于非关键任务(如心跳检测、日志刷新)。
Thread bgThread = new Thread(WorkerMethod);
bgThread.IsBackground = true;
bgThread.Start();
  • 线程状态:通过 ThreadState 枚举表示(Unstarted, Running, WaitSleepJoin, Stopped 等)。
  • 线程池:直接创建和销毁线程开销很大。.NET 提供了一个线程池来管理一组重用的工作线程。

1.2 现代线程管理:System.Threading.Tasks.Task 和 Task Parallel Library (TPL)

从 .NET 4.0 开始,Task 成为了推荐的多线程和异步编程模型。它是对 Thread 的高级封装,极大地简化了复杂操作。

  • 什么是 Task: 表示一个异步操作。它不一定映射到独占的操作系统线程。它可能在线程池线程上运行,也可能使用 I/O 完成端口等机制,效率更高。
  • 线程池: Task 默认使用线程池中的线程。线程池会智能地管理线程数量,根据系统负载创建和销毁线程,避免了频繁创建新线程的开销。
  • 创建和启动 Task
// 方式一:Task.Run (最常用,用于将工作排到线程池)
Task task = Task.Run(() =>
{Console.WriteLine($"Task 在线程池线程上运行。线程ID: {Thread.CurrentThread.ManagedThreadId}");// 模拟工作Thread.Sleep(1000);
});// 方式二:Task.Factory.StartNew (提供更多选项)
Task task2 = Task.Factory.StartNew(() => { /* ... */ }, TaskCreationOptions.LongRunning); // 提示线程池这可能是个长任务// 等待任务完成
task.Wait(); // 阻塞当前线程,直到 task 完成

1.3 状态管理和异常处理

  • 状态查询: Task.Status 属性(Created, WaitingToRun, Running, RanToCompletion, Faulted, Canceled)。
  • 返回值: Task 可以返回值。
Task<int> calculationTask = Task.Run(() => CalculateSomething());
int result = calculationTask.Result; // 获取结果(如果任务未完成,会阻塞当前线程)
  • 异常处理: Task 中的异常会被捕获并存储在 Task.Exception 属性中(一个 AggregateException)。当你调用 .Wait(), .Result, 或 .WaitAll() 时,这些异常会被重新抛出
try
{task.Wait(); // 或者访问 task.Result
}
catch (AggregateException ae)
{foreach (var e in ae.InnerExceptions){Console.WriteLine($"Exception: {e.Message}");}
}

1.4 协调任务:async/await 模式

这是现代 .NET 异步编程的基石,它让异步代码看起来像同步代码一样直观。

  • async: 修饰方法,表明该方法包含异步操作。
  • await: 用于等待一个 Task 完成。在 await 时,当前线程会被释放回线程池,而不是被阻塞。当 Task 完成后,该方法会在线程池线程上恢复执行。
public async Task<int> GetWebsiteLengthAsync(string url)
{// 注意:不要在生产环境使用 HttpClient 这种方式,这里仅为示例。using (var httpClient = new HttpClient()){// await 会释放当前线程(如UI线程),去处理其他工作(如响应用户点击)string content = await httpClient.GetStringAsync(url);// 当下载完成后,执行会在这里恢复(可能在另一个线程池线程上)return content.Length;}
}// 调用异步方法
async void Button_Click(object sender, EventArgs e)
{int length = await GetWebsiteLengthAsync("https://example.com");MessageBox.Show($"Length is: {length}");
}

优势

  • 非阻塞: 在等待 I/O 操作(如网络请求、文件读写)时,不占用任何线程, scalability(可扩展性)极高。
  • 清晰的代码流: 避免了复杂的回调地狱(Callback Hell)。

2. 线程间通信

当多个线程需要访问共享数据或协调行动时,就需要线程间通信。核心挑战是线程安全。

2.1 共享内存与竞态条件

最简单的通信方式是共享变量,但这会导致竞态条件。

private int _counter = 0;void UnsafeIncrement()
{_counter++; // 这不是原子操作,可能被线程切换打断
}

2.2 同步原语:确保线程安全

.NET 提供了丰富的同步原语来控制对共享资源的访问。

  • lock 语句(Monitor 类): 最常用的机制,确保代码块在任何时候只被一个线程执行。
private readonly object _lockObject = new object();
private int _safeCounter = 0;void SafeIncrement()
{lock (_lockObject) // 一次只允许一个线程进入{_safeCounter++;}
}

注意: 锁定对象应为 private readonly 的引用类型。

  • Interlocked 类: 提供简单的原子操作,性能比 lock 更高。
Interlocked.Increment(ref _safeCounter); // 原子性地 +1
Interlocked.Exchange(ref _value, newValue); // 原子性地交换值
  • Mutex 和 Semaphore:
    • Mutex: 类似于 lock,但可以跨进程使用(系统级锁)。
    • Semaphore / SemaphoreSlim: 允许指定数量的线程同时访问一个资源池。例如,限制只有 5 个线程可以同时访问数据库。
  • ManualResetEvent / AutoResetEvent: 用于线程间的信号通知。一个线程可以 WaitOne() 等待信号,另一个线程可以 Set() 发出信号。
  • Barrier / CountdownEvent: 用于协调多个线程,让它们在某个点同步。

2.3 线程安全的数据结构

.NET 在 System.Collections.Concurrent 命名空间中提供了一系列线程安全的集合。

  • ConcurrentDictionary<TKey, TValue>
  • ConcurrentQueue< T>
  • ConcurrentStack< T>
  • ConcurrentBag< T>
  • BlockingCollection< T>

这些集合内部实现了高效的同步机制,可以在大多数情况下避免手动加锁。

private ConcurrentDictionary<string, int> _userScores = new ConcurrentDictionary<string, int>();void UpdateScore(string userId, int points)
{// 无需手动加锁!_userScores.AddOrUpdate(userId, points, (key, oldValue) => oldValue + points);
}

2. 多线程编程模式与最佳实践

2.1 模式

  • 生产者/消费者模式: 一个或多个线程(生产者)生成数据并放入共享队列,一个或多个线程(消费者)从队列中取出并处理数据。可以使用 BlockingCollection 轻松实现。
  • Fork/Join 模式: 将一个大任务拆分成多个小任务(Fork),并行执行,最后等待所有结果并合并(Join)。Parallel.For/ForEach 和 Task.WhenAll 是实现此模式的利器。

2.1 最佳实践与常见陷阱

  1. 避免死锁
    • 原因: 两个或更多线程互相等待对方释放锁。
    • 预防: 按固定的全局顺序获取锁;使用 Monitor.TryEnter 并设置超时;尽量减少锁的持有时间。
  2. 警惕线程池的过度订阅: 不要创建成千上万的短时 Task,这会导致线程池创建大量线程,上下文切换开销巨大。对于 CPU 密集型任务,任务数量不应大幅超过 CPU 核心数。
  3. 不要阻塞线程池线程: 在线程池线程上执行同步的 I/O 操作或长时间 CPU 计算会耗尽线程池,影响整个应用程序的响应能力。对于 I/O 操作,始终使用 async/await。
    4.** 使用 Cancellation Tokens**: 提供一种标准机制来取消异步操作。
var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;Task longRunningTask = Task.Run(() =>
{while (true){token.ThrowIfCancellationRequested(); // 如果取消请求了,则抛出 OperationCanceledException// ... 做一点工作}
}, token);// 在某个地方取消操作
cts.CancelAfter(5000); // 5秒后取消
  1. 访问 UI 控件: 在 WPF/WinForms 中,只有 UI 线程才能更新 UI 控件。从非 UI 线程更新 UI 会引发异常。必须使用 Dispatcher.Invoke (WPF) 或 Control.Invoke (WinForms) 来封送调用回 UI 线程。
// 在 WPF 中
await Task.Run(() => DoHeavyWork());
// 现在回到 UI 线程了,可以安全更新 UI
textBox.Text = "Done!";// 如果在另一个上下文中,需要显式调用 Dispatcher
Dispatcher.Invoke(() => { textBox.Text = "Done!"; });

总结

  • 基础: 理解 Thread 类。
  • 现代方式: 优先使用 Task 和 TPL,默认使用线程池,效率更高。
  • 异步 I/O: 对于 I/O 密集型操作,始终使用 async/await,以释放线程,获得极高的可扩展性。
  • 线程安全: 使用 lock、Interlocked 或并发集合来保护共享数据。
  • 协调与通信: 使用同步原语(如 Event、Barrier)和模式(生产者/消费者)来协调多线程工作。
  • 避免陷阱: 警惕死锁、过度订阅和阻塞线程池线程。

2.2 补充


线程间通信的本质是什么?
多个线程在同一个进程内运行,共享进程的整个内存空间。因此,从广义上讲,任何一个线程写入内存的数据,理论上都可以被其他线程读取到。

所以,线程间通信的“通信”二字,其本质是:

  1. 数据传递:一个线程生产/计算出的数据,如何安全地交给另一个线程处理。
  2. 状态同步:一个线程如何知道另一个线程已经完成了某项工作或进入了某种状态。
  3. 协调行动:多个线程如何步调一致地协作,避免“混乱”(如竞态条件)和“死等”(如死锁)。

核心挑战:由于操作系统线程调度的不确定性,你永远不知道一个线程在执行到哪条指令时会被挂起,另一个线程会开始执行。这种交错执行如果处理不当,就会导致数据损坏、结果错误等线程安全问题。

如何进行线程间通信?
.NET 提供了多种机制来实现安全高效的线程间通信,主要分为三大类:

  1. 共享内存(最常用,但最危险)
    这是最直观的方式:多个线程读写同一个变量或数据结构。
  • 如何进行:简单地创建一个所有线程都能访问的字段、属性或静态变量。
  • 巨大风险:直接共享内存会引发竞态条件。
// 危险的共享内存示例
public class UnsafeExample
{private int _counter = 0; // 共享内存public void Increment(){_counter++; // 这不是原子操作!// 它可能被分解为:读取 -> 加1 -> 写入// 线程A可能在“读取”后被打断,线程B也完成了“读取”,然后两者都写入,导致只加了一次。}
}
  • 如何安全地使用:必须使用同步原语来保护对共享内存的访问,确保某一时刻只有一个线程能操作它。

    • lock 语句:最常用的工具。
    private readonly object _lockObj = new object();
    private int _safeCounter = 0;public void SafeIncrement()
    {lock (_lockObj) // 一次只允许一个线程进入此代码块{_safeCounter++;}
    }
    
    • Interlocked 类:为简单的数学操作提供原子性,性能更高。
    Interlocked.Increment(ref _safeCounter); // 原子性地完成整个“读取-修改-写入”操作
    
    • Monitor 类:lock 语句的底层实现。
    • Mutex:类似于锁,但可以跨进程使用。
  1. 信号机制(用于协调和通知)
    当一个线程需要“等待”另一个线程完成某项工作后才能继续时,就需要信号机制。它不直接传递数据,而是传递“事件已发生”的信号。
  • 如何进行:一个线程等待一个信号,另一个线程发出信号。
  • 常见类型:
    • EventWaitHandle 及其子类:
      • AutoResetEvent:像一个旋转门,一次只允许一个线程通过。Set() 一次只释放一个等待的线程,然后自动重置为无信号状态。
      • ManualResetEvent:像一个大门,Set() 打开大门,释放所有等待的线程;直到调用 Reset() 才会关上大门。
        // 使用 AutoResetEvent 进行线程协调
        AutoResetEvent _waitHandle = new AutoResetEvent(false); // 初始状态为无信号void ThreadA()
        {// 做一些准备工作..._waitHandle.Set(); // 发出信号:“我的工作完成了,你可以继续了”
        }void ThreadB()
        {// 等待 ThreadA 的准备信号_waitHandle.WaitOne(); // 阻塞在此,直到收到信号// 收到信号,继续执行...
        }
        
    • Semaphore / SemaphoreSlim:类似于一个计数器,用于控制同时访问某一资源的线程数量。例如,只允许 3 个线程同时访问数据库连接池。
    • Barrier:用于让多个线程在某个时间点同步,所有线程都到达这个点后,才一起继续执行。适合分阶段计算的场景。
    • CountdownEvent:初始化一个计数,每次有线程完成工作时计数减一,当计数为 0 时,释放所有等待的线程。
  1. 消息传递(更高级、更安全的模式)
    这种模式解耦了线程,线程之间不直接共享内存,而是通过一个“中间人”(通常是队列)来传递数据“消息”。生产者线程放入消息,消费者线程取出消息。
  • 如何进行:使用生产者/消费者模式。
  • .NET 提供的强大工具:System.Collections.Concurrent 命名空间下的线程安全集合。
    • BlockingCollection:一个提供了阻塞和边界功能的线程安全集合。它是实现生产者/消费者模式的最佳工具。

      // 创建一个最多容纳10个项目的阻塞集合
      BlockingCollection<string> _messageQueue = new BlockingCollection<string>(10);// 生产者线程
      void Producer()
      {while (true){string message = GenerateMessage();_messageQueue.Add(message); // 如果队列满了,Add 会阻塞生产者}_messageQueue.CompleteAdding(); // 通知消费者不会再生产了
      }// 消费者线程
      void Consumer()
      {// GetConsumingEnumerable() 会在没有数据时阻塞消费者,并在 CompleteAdding() 且队列空后自动结束foreach (var message in _messageQueue.GetConsumingEnumerable()){ProcessMessage(message);}
      }
      
    • ConcurrentQueue, ConcurrentStack, ConcurrentBag, ConcurrentDictionary<TKey, TValue>:这些是线程安全的集合,可以在不加锁的情况下被多个线程同时读写,但它们本身不提供阻塞功能。

总结与最佳实践

通信机制如何实现适用场景优点缺点
共享内存共享变量 + 锁/同步原语高频、简单的数据共享性能高、直观容易死锁、难以编写和维护
信号机制EventWaitHandle, Semaphore, Barrier线程间的协调、通知、同步目的明确,易于理解不直接传递数据,容易错过信号
消息传递BlockingCollection< T > + 并发集合生产者/消费者、解耦复杂任务安全性高、解耦、易于扩展有一定的性能开销(入队/出队)

现代 .NET 多线程编程的最佳实践:

  1. 优先选择消息传递模式:使用 BlockingCollection 或 Channel (.NET Core 3.0+) 可以极大地减少对锁的依赖,从而避免死锁等问题,代码也更清晰。

  2. 避免共享状态:如果可能,尽量设计无状态的操作,让每个线程只处理自己的数据。

  3. 使用高级抽象:优先使用 Task、Parallel 循环和 PLINQ,而不是手动管理 Thread 对象。它们底层使用线程池,效率更高。

  4. 善用异步编程:对于 I/O 密集型操作(如文件、网络),使用 async/await 而不是创建阻塞线程,这样可以释放线程去处理其他请求,大大提高应用程序的吞吐量。

  5. 始终牢记线程安全:只要存在共享,第一反应就应该是“如何同步”。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/96060.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/96060.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(JVM)四种垃圾回收算法

在 JVM 中&#xff0c;垃圾回收&#xff08;GC&#xff09;是核心机制之一。为了提升性能与内存利用率&#xff0c;JVM 采用了多种垃圾回收算法。本文总结了 四种常见的 GC 算法&#xff0c;并结合其优缺点与应用场景进行说明。1. 标记-清除&#xff08;Mark-Sweep&#xff09;…

论文阅读:VGGT Visual Geometry Grounded Transformer

论文阅读&#xff1a;VGGT: Visual Geometry Grounded Transformer 今天介绍一篇 CVPR 2025 的 best paper&#xff0c;这篇文章是牛津大学的 VGG 团队的工作&#xff0c;主要围绕着 3D 视觉中的各种任务&#xff0c;这篇文章提出了一种多任务统一的架构&#xff0c;实现一次输…

python编程:一文掌握pypiserver的详细使用

更多内容请见: python3案例和总结-专栏介绍和目录 文章目录 一、 pypiserver 概述 1.1 pypiserver是什么? 1.2 核心特性 1.3 典型应用场景 1.4 pypiserver优缺点 二、 安装与基本使用 2.1 安装 pypiserver 2.2 快速启动(最简模式) 2.3 使用私有服务器安装包 2.4 向私有服务…

Git reset 回退版本

- 第 121 篇 - Date: 2025 - 09 - 06 Author: 郑龙浩&#xff08;仟墨&#xff09; 文章目录Git reset 回退版本1 介绍三种命令区别3 验证三种的区别3 如果不小心git reset --hard将「工作区」和「暂存区」中的内容删除&#xff0c;刚才的记录找不到了&#xff0c;怎么办呢&…

ARM 基础(2)

ARM内核工作模式及其切换条件用户模式(User Mode, usr) 权限最低&#xff0c;运行普通应用程序。只能通过异常被动切换到其他模式。快速中断模式(FIQ Mode, fiq) 处理高速外设中断&#xff0c;专用寄存器减少上下文保存时间&#xff0c;响应周期约4个时钟周期。触发条件为FIQ中…

Flutter 性能优化

Flutter 性能优化是一个系统性的工程&#xff0c;涉及多个层面。 一、性能分析工具&#xff08;Profiling Tools&#xff09; 在开始优化前&#xff0c;必须使用工具定位瓶颈。切忌盲目优化。 1. DevTools 性能视图 DevTools 性能视图 (Performance View) 作用&#xff1a;…

Spring事件监听机制(三)

为了理解EvenListener注解的底层原理&#xff0c;我们可以自己实现一个类似的注解模拟实现。1.定义MyListener注解Target({ElementType.METHOD})Retention(RetentionPolicy.RUNTIME)public interface MyListener {}2.注解使用Componentstatic class SmsService {private static…

基于Springboot + vue3实现的小区物业管理系统

项目描述本系统包含管理员和用户两个角色。管理员角色&#xff1a;用户管理&#xff1a;管理系统中所有用户的信息&#xff0c;包括添加、删除和修改用户。房屋信息管理&#xff1a;管理房屋信息&#xff0c;包括新增、查看、修改和删除房屋信息。车辆信息管理&#xff1a;管理…

交叉熵和KL散度

这个问题之前我也是傻傻分不清&#xff0c;决定整理一下&#xff0c;用更印象深刻的方式让人记住。核心联系&#xff1a;交叉熵 KL 散度 真实分布的熵 交叉熵作为 “绝对” 度量&#xff0c;会综合真实分布的熵&#xff08;固有难度&#xff09;与预测误差&#xff0c;直接体…

HTML 各种事件的使用说明书

HTML 各种事件的使用说明书 1. HTML 事件简介 HTML事件是浏览器或用户在网页上执行的动作或发生的事情。当这些事件发生时&#xff0c;可以通过JavaScript来响应和处理这些事件&#xff0c;从而实现网页的交互功能。事件处理是Web前端开发中实现动态交互的核心机制。 基本概…

Kafka面试精讲 Day 10:事务机制与幂等性保证

【Kafka面试精讲 Day 10】事务机制与幂等性保证 在分布式消息系统中&#xff0c;如何确保消息不丢失、不重复&#xff0c;是系统可靠性的核心挑战。Kafka自0.11版本起引入了幂等性Producer和事务性消息机制&#xff0c;彻底解决了“至少一次”语义下可能产生的重复消息问题&am…

时序数据库简介和安装

一、简介1. 什么是时序数据库&#xff1f;时序数据库是专门用于存储和处理时间序列数据的数据库系统。时间序列数据是指按时间顺序索引的一系列数据点。每个数据点都包含&#xff1a;一个时间戳&#xff1a;记录数据产生的时间。一个或多个指标值&#xff1a;例如温度、湿度、C…

comfyUI 暴露网络restful http接口

https://zhuanlan.zhihu.com/p/686893291 暴露websocket接口。 打开开发者选项 如图

linux系统address already in use问题解决

linux系统上某个端口被占用&#xff0c;如何解决&#xff1f;1.找到占用的进程编号&#xff1a;netstat -tulnp | grep :80002.强制杀死该进程kill -9 80603其他说明&#xff1a;1.查找占用端口的进程&#xff0c;可以用&#xff1a;lsof -i :8001 # 或者使用 netstat -tulnp |…

基于SpringBoot的家政保洁预约系统【计算机毕业设计选题 计算机毕业设计项目 计算机毕业论文题目推荐】

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

【Linux系统】 4. 权限(一)

一. shell 命令及运行原理基本理解1&#xff09;广义理解的操作系统包括&#xff1a;操作系统内核、外壳程序&#xff08;shell命令行、图形化界面&#xff09;、必要的软件。2&#xff09;狭义的操作系统&#xff1a;操作系统内核。3&#xff09;在用户和内核之间有一个外壳程…

6.python——字符串

python中用’ 和" "创建字符串 python的子字符串截取用[]取字符串拼接可以直接用相加。 python三引号允许一个字符串跨多行&#xff0c;其中无需进行转义&#xff08;所见即所得&#xff09;。 当你需要一块HTML或者SQL时&#xff0c;这时用字符串组合&#xff0c;特…

足球数据API接口的技术特性与应用价值分析

一、接口概述现代足球数据接口是基于RESTful架构的数据服务&#xff0c;通过标准化方式提供赛事相关信息。这类接口通常采用JSON格式传输数据&#xff0c;支持跨平台调用&#xff0c;为开发者提供结构化的足球赛事数据。二、数据覆盖范围主流足球数据接口通常包含以下数据类型&…

<android>反编译魔改安卓系统应用并替换

我们知道安卓系统基于稳定性、维护便利、性能优化等原因并未对原生系统apk进行混淆加密处理&#xff0c;由此就方便了我们反编译替换原生应用。 首先我们设备需要是root后的&#xff0c;我是使用的是小米5&#xff0c;刷的24.3版本的面具。首先我们需要取系统apk&#xff0c;这…

【Qt】项目的创建 and 各个控件的使用

一、项目的创建&#x1f50d;然后点击新建项目。&#x1f4d6;注意&#xff1a;路径不要带有中文&#xff0c;不然运行不了代码。&#x1f4d6;qmake是一个构建工具&#xff0c;在 Qt 写的程序&#xff0c;设计的到一系列的 "元编程" 技术&#xff0c;什么是元编程技…