二十六、线程与并发(Thread Basic)

📘 第26章:线程与并发

多线程和并发编程在现代计算机应用中占有至关重要的位置。随着多核处理器的普及,我们的应用程序不仅要支持并发执行,还要高效地管理多线程任务。《CLR via C#》第26章深入探讨了如何在 C# 中使用线程、线程池、任务(Task)以及并行编程库(TPL)来实现高效的并发处理。本文将带你全面了解 C# 中线程和并发编程的实现方式,帮助你写出更高效、更安全的多线程应用。

1.为什么 Windows 支持线程?

在操作系统中,线程是最基本的执行单元,能够让计算机同时执行多个任务。Windows 支持线程的核心原因是:

  • 并发执行:通过多线程,操作系统能够同时处理多个任务,特别是在多核处理器上,线程能够在不同的核心上并行执行任务,从而提高性能。
  • 响应性:将耗时的操作放到后台线程执行,避免主线程被阻塞,从而提升应用程序的响应速度。
  • 任务隔离:每个线程都有自己的堆栈空间,任务之间互不干扰,避免了共享内存导致的问题。

Mermaid 图示:线程的作用

graph LR A[Windows 支持线程] --> B[并发执行任务] B --> C[多核处理器加速] A --> D[响应性提升] A --> E[任务隔离]

2. 线程的开销

每个线程在创建和销毁时都会消耗一定的系统资源,主要体现在:

  • 内存开销:每个线程需要分配一定的内存空间来存储堆栈和执行上下文。线程数过多时,会导致内存消耗过大。
  • 上下文切换:当操作系统在多个线程之间切换时,会保存当前线程的状态,并加载下一个线程的状态。这一过程消耗 CPU 时间,频繁的上下文切换会导致性能下降。

如何优化线程开销?

  • 使用 线程池(ThreadPool)来避免频繁创建和销毁线程。
  • 控制线程数量,避免创建过多的线程导致资源耗尽。
// 使用线程池执行任务,避免频繁创建和销毁线程
ThreadPool.QueueUserWorkItem(DoWork);

private void DoWork(object state)
{
    // 执行任务
}

3. 停止疯狂的线程创建

频繁创建和销毁线程会带来性能瓶颈。为了解决这个问题,可以使用 线程池 来复用线程,而不是每次都创建新的线程。

线程池的优势:

  • 自动管理线程:线程池自动调整线程数量,避免手动管理线程的开销。
  • 线程复用:线程池中的线程可以重复使用,避免了创建和销毁线程的性能损耗。
// 使用线程池执行计算密集型任务
ThreadPool.QueueUserWorkItem(CalculateTask);

private void CalculateTask(object state)
{
    // 计算密集型操作
}

4. CPU 趋势

随着硬件技术的发展,尤其是 多核 CPU 的普及,线程的作用变得越来越重要。现代计算机通常配备多个核心,线程可以在不同的核心上并行执行任务,从而显著提高程序的处理能力。

面临的挑战:

  • 线程调度:如何合理分配任务到不同的核心,确保高效计算。
  • 任务同步:如何确保多个线程之间的数据同步,避免竞态条件。

Mermaid 图示:多核处理器

graph TD A[多核处理器] --> B[线程并行执行] B --> C[提高计算效率] B --> D[合理分配任务] D --> E[减少上下文切换]

五、🎯 线程与并发编程基础

线程是操作系统调度的最小单位,它代表了程序执行的独立路径。每个应用程序都有一个主线程,程序中的其他任务可以在多个线程中并行执行,从而实现并发处理。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.ForParallel.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 类提供了 ContinueWithWhenAll 方法来处理多个任务的异常。

Task.Run(() => {
    throw new InvalidOperationException("Error in task");
}).ContinueWith(task => {
    Console.WriteLine($"Task failed with: {task.Exception}");
}, TaskContinuationOptions.OnlyOnFaulted);

🧩 并发编程中的挑战与最佳实践

  1. 数据竞态与死锁:并发程序必须小心设计,避免数据竞态和死锁问题。使用锁时,尽量避免嵌套锁,确保避免死锁。
  2. 任务取消:通过 CancellationToken 控制任务执行,可以中断长时间运行的任务。
  3. 错误处理:多线程环境中异常的捕获非常重要。使用 try-catch 块捕获异步任务中的异常,并进行适当的错误处理。
  4. 合理使用线程池:线程池能够减少线程创建和销毁的开销,避免线程的过度创建。合理使用 Task 和线程池进行任务调度。

📌 总结:C# 中的并发与线程管理

特性 说明
线程池 通过复用线程池线程,提高性能,减少开销
任务并行库(TPL) 简化并行操作的管理,易于实现高并发任务
线程安全集合类 内建并发控制,减少开发者手动同步的复杂度
线程同步机制 使用 lockMonitorMutex 等保证线程安全
并行编程(Parallel) 通过 Parallel.For 等方法简化并行任务执行

image-20250820105006884

🎯 Unity 相关面试题

Unity 面试题(含解析)

Q1:如何在 Unity 中避免主线程被阻塞,提升响应性?

  • 答案:使用 async/await 异步编程,或者通过后台线程(如 ThreadTask)执行耗时操作,避免主线程阻塞。

Q2:如何在 Unity 中管理多个线程?

  • 答案:使用线程池(ThreadPool)或并行任务(Task)来管理多个线程,避免频繁创建和销毁线程。

Q3:Unity 的主线程与后台线程有什么不同?

  • 答案:主线程负责游戏的核心逻辑和 UI 更新,后台线程负责处理耗时任务,如数据加载、AI 运算等,后台线程不能直接访问 Unity 的 API。

Q4:如何在 Unity 中使用后台线程进行数据处理?

  • 答案:通过创建 ThreadTask 来执行数据处理任务,并确保后台线程不直接访问 Unity 的对象和 API。

Q5:在 Unity 中使用线程时应该注意哪些问题?

  • 答案:避免在后台线程中直接访问 Unity 对象和 API,使用线程安全的队列或锁来同步数据,并通过主线程更新 UI。
posted @ 2025-08-26 10:08  世纪末の魔术师  阅读(8)  评论(0)    收藏  举报