二十六、线程与并发(Thread Basic)
📘 第26章:线程与并发
多线程和并发编程在现代计算机应用中占有至关重要的位置。随着多核处理器的普及,我们的应用程序不仅要支持并发执行,还要高效地管理多线程任务。《CLR via C#》第26章深入探讨了如何在 C# 中使用线程、线程池、任务(Task)以及并行编程库(TPL)来实现高效的并发处理。本文将带你全面了解 C# 中线程和并发编程的实现方式,帮助你写出更高效、更安全的多线程应用。
1.为什么 Windows 支持线程?
在操作系统中,线程是最基本的执行单元,能够让计算机同时执行多个任务。Windows 支持线程的核心原因是:
- 并发执行:通过多线程,操作系统能够同时处理多个任务,特别是在多核处理器上,线程能够在不同的核心上并行执行任务,从而提高性能。
 - 响应性:将耗时的操作放到后台线程执行,避免主线程被阻塞,从而提升应用程序的响应速度。
 - 任务隔离:每个线程都有自己的堆栈空间,任务之间互不干扰,避免了共享内存导致的问题。
 
Mermaid 图示:线程的作用
2. 线程的开销
每个线程在创建和销毁时都会消耗一定的系统资源,主要体现在:
- 内存开销:每个线程需要分配一定的内存空间来存储堆栈和执行上下文。线程数过多时,会导致内存消耗过大。
 - 上下文切换:当操作系统在多个线程之间切换时,会保存当前线程的状态,并加载下一个线程的状态。这一过程消耗 CPU 时间,频繁的上下文切换会导致性能下降。
 
如何优化线程开销?
- 使用 线程池(ThreadPool)来避免频繁创建和销毁线程。
 - 控制线程数量,避免创建过多的线程导致资源耗尽。
 
// 使用线程池执行任务,避免频繁创建和销毁线程
ThreadPool.QueueUserWorkItem(DoWork);
private void DoWork(object state)
{
    // 执行任务
}
3. 停止疯狂的线程创建
频繁创建和销毁线程会带来性能瓶颈。为了解决这个问题,可以使用 线程池 来复用线程,而不是每次都创建新的线程。
线程池的优势:
- 自动管理线程:线程池自动调整线程数量,避免手动管理线程的开销。
 - 线程复用:线程池中的线程可以重复使用,避免了创建和销毁线程的性能损耗。
 
// 使用线程池执行计算密集型任务
ThreadPool.QueueUserWorkItem(CalculateTask);
private void CalculateTask(object state)
{
    // 计算密集型操作
}
4. CPU 趋势
随着硬件技术的发展,尤其是 多核 CPU 的普及,线程的作用变得越来越重要。现代计算机通常配备多个核心,线程可以在不同的核心上并行执行任务,从而显著提高程序的处理能力。
面临的挑战:
- 线程调度:如何合理分配任务到不同的核心,确保高效计算。
 - 任务同步:如何确保多个线程之间的数据同步,避免竞态条件。
 
Mermaid 图示:多核处理器
五、🎯 线程与并发编程基础
线程是操作系统调度的最小单位,它代表了程序执行的独立路径。每个应用程序都有一个主线程,程序中的其他任务可以在多个线程中并行执行,从而实现并发处理。C# 为开发者提供了 System.Threading 命名空间,允许开发者创建、管理和同步线程。
线程的创建和启动
线程的基本创建过程相对简单,可以使用 Thread 类来启动一个新的线程。
Thread t = new Thread(new ThreadStart(SomeMethod));
t.Start();
ThreadStart 是一个委托,指向一个方法,该方法将在新线程上执行。线程一旦启动,就开始执行指定的任务。
⚙️ 线程池:高效的线程管理
ThreadPool 类允许 C# 管理一组线程,在需要时复用它们,而无需频繁创建和销毁线程。线程池是通过多线程来优化性能的关键技术,避免了传统线程创建的开销。
使用线程池执行任务:
ThreadPool.QueueUserWorkItem(WorkMethod);
QueueUserWorkItem 方法将指定的任务添加到线程池队列中。线程池的线程会自动取出任务并执行。这种方式非常适用于处理大量短小的并行任务。
线程池的优势:
- 线程复用:避免了线程创建的高开销。
 - 线程管理自动化:开发者无需手动管理线程的生命周期,线程池会自动处理。
 
🧵 并行编程(Parallel Programming)与任务(Task)
随着并行编程需求的增长,C# 提供了 Task 类和 Parallel 类来简化并行操作。
1. Task 类:
Task 是 .NET 的并发编程核心,用于执行后台任务、异步操作以及并行计算。与传统的线程模型不同,Task 代表了一个异步操作,而不是直接表示一个线程。
Task.Run(() => {
    Console.WriteLine("Running in background thread");
});
使用 Task 类可以轻松地实现异步任务的执行,并且在任务完成时可以继续处理结果。Task.WhenAll 可以并行执行多个任务并等待它们全部完成。
Task[] tasks = new Task[3];
for (int i = 0; i < 3; i++) {
    tasks[i] = Task.Run(() => {
        Console.WriteLine($"Task {i} completed");
    });
}
Task.WhenAll(tasks).Wait();  // 等待所有任务完成
2. Parallel 类:
Parallel 类提供了更加简便的并行处理方法,常用于需要并行执行相同操作的场景。
Parallel.For(0, 10, i => {
    Console.WriteLine($"Processing {i}");
});
Parallel.For 和 Parallel.ForEach 方法用于并行处理循环和集合,能够显著提升性能,尤其是处理大量独立任务时。
🛠 线程同步与锁机制
多线程编程常见的问题是数据竞态(Race Condition)。当多个线程并发访问共享数据时,如果没有适当的同步控制,就可能导致数据不一致。
1. 锁(Lock)
C# 提供了 lock 关键字用于简化线程同步,防止多个线程同时进入临界区。
private static readonly object lockObj = new object();
public void SafeMethod() {
    lock (lockObj) {
        // 线程安全的操作
    }
}
lock 确保了只有一个线程能够进入临界区,其他线程需要等待释放锁才能执行。
2. 其他同步机制
除了 lock,C# 还提供了多种同步机制:
- Monitor:更细粒度的锁控制,支持等待和信号操作。
 - Mutex:跨进程锁机制。
 - Semaphore:控制并发线程数量。
 
通过这些同步工具,开发者可以有效地管理线程之间的共享资源。
⚖️ 线程安全的集合类
对于需要在多线程环境中频繁操作的集合,.NET 提供了线程安全的集合类,诸如 ConcurrentQueue<T> 和 ConcurrentDictionary<K, V>。这些集合类在设计时考虑了并发问题,避免了开发者手动同步的复杂性。
ConcurrentQueue<int> queue = new ConcurrentQueue<int>();
queue.Enqueue(42);
queue.TryDequeue(out int result);
这些集合类在并发操作时具有更好的性能,特别是在多线程环境中。
🚀 高级并发控制:任务取消与异常处理
在复杂的并发程序中,任务取消和异常管理非常重要。C# 提供了 CancellationToken 来优雅地取消任务,避免任务无限期运行。
示例:任务取消
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Task.Run(() => {
    for (int i = 0; i < 10; i++) {
        if (token.IsCancellationRequested) {
            Console.WriteLine("Task was canceled.");
            break;
        }
        Console.WriteLine(i);
        Thread.Sleep(1000);
    }
});
cts.Cancel();  // 请求取消任务
异常处理
在并行任务中,异常管理至关重要。Task 类提供了 ContinueWith 和 WhenAll 方法来处理多个任务的异常。
Task.Run(() => {
    throw new InvalidOperationException("Error in task");
}).ContinueWith(task => {
    Console.WriteLine($"Task failed with: {task.Exception}");
}, TaskContinuationOptions.OnlyOnFaulted);
🧩 并发编程中的挑战与最佳实践
- 数据竞态与死锁:并发程序必须小心设计,避免数据竞态和死锁问题。使用锁时,尽量避免嵌套锁,确保避免死锁。
 - 任务取消:通过 
CancellationToken控制任务执行,可以中断长时间运行的任务。 - 错误处理:多线程环境中异常的捕获非常重要。使用 
try-catch块捕获异步任务中的异常,并进行适当的错误处理。 - 合理使用线程池:线程池能够减少线程创建和销毁的开销,避免线程的过度创建。合理使用 
Task和线程池进行任务调度。 
📌 总结:C# 中的并发与线程管理
| 特性 | 说明 | 
|---|---|
| 线程池 | 通过复用线程池线程,提高性能,减少开销 | 
| 任务并行库(TPL) | 简化并行操作的管理,易于实现高并发任务 | 
| 线程安全集合类 | 内建并发控制,减少开发者手动同步的复杂度 | 
| 线程同步机制 | 使用 lock、Monitor、Mutex 等保证线程安全 | 
| 并行编程(Parallel) | 通过 Parallel.For 等方法简化并行任务执行 | 

🎯 Unity 相关面试题
Unity 面试题(含解析)
Q1:如何在 Unity 中避免主线程被阻塞,提升响应性?
- 答案:使用 
async/await异步编程,或者通过后台线程(如Thread或Task)执行耗时操作,避免主线程阻塞。 
Q2:如何在 Unity 中管理多个线程?
- 答案:使用线程池(
ThreadPool)或并行任务(Task)来管理多个线程,避免频繁创建和销毁线程。 
Q3:Unity 的主线程与后台线程有什么不同?
- 答案:主线程负责游戏的核心逻辑和 UI 更新,后台线程负责处理耗时任务,如数据加载、AI 运算等,后台线程不能直接访问 Unity 的 API。
 
Q4:如何在 Unity 中使用后台线程进行数据处理?
- 答案:通过创建 
Thread或Task来执行数据处理任务,并确保后台线程不直接访问 Unity 的对象和 API。 
Q5:在 Unity 中使用线程时应该注意哪些问题?
- 答案:避免在后台线程中直接访问 Unity 对象和 API,使用线程安全的队列或锁来同步数据,并通过主线程更新 UI。
 
作者:世纪末的魔术师
        
               出处:https://www.cnblogs.com/Firepad-magic/
        
               Unity最受欢迎插件推荐:点击查看
        
        本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
    
                    
                
                
            
        
浙公网安备 33010602011771号