C#.NET多线程编程实战:6个实例深入理解

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:C#在.NET框架中提供了强大的多线程编程能力,使得开发者能够利用多核处理器进行并行处理。本资源包含了六个涵盖多线程基础和高级特性的C#.NET实例,如线程创建、线程同步与互斥、线程池使用、线程同步原语、异步编程模型、线程通信、线程状态管理、异常处理、线程局部存储和线程优先级等。开发者通过运行和分析这些实例,可以深入学习和掌握C#.NET多线程编程的实战应用。

1. C#.NET多线程基础使用

在现代软件开发中,多线程是一种必不可少的技术,它可以显著提升程序的性能和用户体验。C#.NET 作为微软推出的一种强大的编程语言,其提供了丰富的多线程编程模型,允许开发者利用.NET框架提供的线程管理机制,创建高效且稳定的应用程序。

在本章中,我们将初步了解多线程的基础知识,包括线程的概念、如何创建线程以及多线程编程的基本模式。我们将探讨.NET中的线程基础,比如线程的生命周期,以及如何安全地使用线程。此外,我们还将涵盖一些高级主题,比如线程同步和异步编程模型的基础。通过这些基础概念的理解,开发者可以为后续章节中线程的深入讨论打下坚实的基础。

2. 线程创建方法

2.1 基于Thread类创建线程

2.1.1 Thread类的构造与初始化

在.NET中, Thread 类提供了最基础的多线程创建机制。通过该类,可以直接创建和启动线程。Thread类的构造函数有多个重载形式,允许开发者在创建线程时就配置不同的参数。最基本的是无参构造函数,它允许开发者在之后通过 ThreadStart 委托或 ParameterizedThreadStart 委托来指定线程要执行的方法。以下是创建一个简单的Thread对象的基本步骤:

Thread thread = new Thread(new ThreadStart(MyMethod));

在上面的代码中, MyMethod 是线程将要执行的方法,必须符合无参数且无返回值的签名。

2.1.2 启动与控制线程的生命周期

创建线程后,我们可以调用 Start() 方法来启动线程的执行。这个方法会立即返回,不会等待线程结束。此外, Thread 类还提供了控制线程生命周期的方法,例如 Sleep(int millisecondsTimeout) 可以让当前线程暂停指定的时间, Abort() 则用于终止线程的执行。

thread.Start(); // 启动线程
// thread.Abort(); // 注意:强烈不推荐使用Abort方法,因为它会产生未处理的异常。

2.2 基于Task并行库创建线程

2.2.1 Task类与线程池的关系

Task 类代表一个异步操作,它是在.NET 4.0中引入的并行编程模型的一部分,即Task Parallel Library (TPL)。使用 Task 类相比传统的 Thread 类,可以更好地利用系统资源,因为 Task 类是基于线程池的,它避免了频繁创建和销毁线程带来的开销。

Task task = Task.Run(() => MyMethod()); // 使用Task并行库创建线程

2.2.2 Task的创建与执行

创建和执行Task很简单,只需调用 Task.Run() 方法,传入一个 Action 委托或一个返回 Task Task<T> 的工厂方法。 Task.Run() 方法的背后实际上是调用 ThreadPool.QueueUserWorkItem() 来将任务委托给线程池。

2.2.3 Task与线程返回值的处理

Task 类还支持返回值,这对于需要从异步操作中获取结果的场景非常有用。可以通过 Task<T> 来实现这一点。

Task<int> task = Task.Run(() =>
{
    int result = MyMethod(); // 执行方法,假设返回int类型
    return result;
});

int result = await task; // 等待任务完成,并获取返回值

在上面的代码中,我们创建了一个返回 int 类型的 Task 。使用 await 关键字可以让当前线程暂停执行,直到 Task 完成,然后获取其返回值。

通过对比传统Thread类和Task并行库,我们能够看出它们在创建线程和管理线程生命周期上的不同。Task并行库在简化代码和提高性能方面提供了显著的优势,是推荐在.NET程序中使用的线程创建方式。

3. 线程同步与互斥机制

3.1 线程间竞争条件与同步需求

3.1.1 竞争条件的识别与危害

在多线程环境中,多个线程几乎同时访问和修改共享资源的情况极为常见,导致竞争条件的出现。竞争条件是指多个线程的执行时序依赖于非确定因素,如线程调度的时机和顺序,从而产生不可预期的结果。

识别竞争条件的一个典型示例是银行账户的余额处理。考虑两个线程同时对同一账户的余额进行操作:一个负责存款,一个负责取款。假设账户余额初始为1000元,如果两个线程几乎同时运行,那么可能出现以下不正确的结果:

  • 线程A读取余额(1000元),计算增加200元。
  • 线程B也读取余额(1000元),计算减少300元。
  • 线程A将新的余额(1200元)存回账户。
  • 线程B将新的余额(700元)存回账户。

最终账户的余额将错误地为700元,而不是预期的900元,因为两次操作没有正确地顺序执行,这便是竞争条件的危害。

3.1.2 同步机制的基本原理

为了避免上述竞争条件,同步机制应运而生。同步机制的基本原理是确保在任何给定时刻,只有一个线程能够访问或修改共享资源。这通常通过锁机制来实现,它允许线程在访问资源前获取一个唯一的访问令牌(锁),其他线程在锁被占用时必须等待,直到该锁被释放。

同步机制有助于:

  • 保护共享资源不被并发访问所破坏。
  • 防止数据不一致性,保证数据的完整性。
  • 提供有序访问共享资源,避免资源竞争。

常见的同步原语包括互斥锁(Mutex)、监视器(Monitor)、信号量(Semaphore)、读写锁(ReaderWriterLockSlim)以及锁语句(lock)等。

3.2 关键同步原语使用详解

3.2.1 Mutex的工作原理与应用

Mutex是一种同步原语,用于控制对共享资源的互斥访问。 它可以是命名的也可以是非命名的。命名Mutex允许跨进程边界的同步,而非命名Mutex仅在同一个进程内的线程间有效。

Mutex的工作原理:

  • 当一个线程获得一个Mutex时,它被标记为拥有Mutex的"所有者"。
  • 如果其他线程尝试获取已经被拥有的Mutex,它们将被阻塞,直到Mutex被释放。
  • 在Mutex被释放之后,等待列表中的下一个线程将获得所有权,并继续执行。

Mutex的应用场景:

  • 在需要限制对某个资源的访问次数时使用,例如限制同时运行的进程数量。
  • 在需要跨进程同步时使用。

3.2.2 Monitor的锁定机制与使用场景

Monitor提供了一种锁机制,用于在运行时管理对对象的互斥访问。 它通过锁定和解锁对象来实现同步。

Monitor的锁定机制:

  • 使用 Monitor.Enter 方法锁定对象。
  • 当锁定成功, Monitor保持锁定状态直到调用 Monitor.Exit 方法或者通过 Monitor.Wait 方法释放锁。
  • 当一个线程等待一个已经被其他线程锁定的对象时,该线程会被阻塞,直到对象被释放。

Monitor的使用场景:

  • 当需要对代码块的执行提供排他性控制时使用。
  • 用于保护临界区(Critical Section),确保一次只有一个线程执行。
  • 适用于对象同步,因为Monitor直接和对象实例关联。

3.2.3 Semaphore的计数信号量功能

Semaphore是一种计数信号量,可以用来控制同时访问资源的线程数量。

Semaphore的工作原理:

  • 它维护一组许可证,线程在进入临界区前必须获取一个许可证,离开时释放许可证。
  • 当许可证的数量减到0时,额外的线程必须等待,直到有许可证可用。

Semaphore的应用场景:

  • 当需要控制对资源池或资源数量有限的资源的访问时。
  • 在多线程网络服务器中,限制同时处理的客户端数量。

3.2.4 ReaderWriterLockSlim的读写锁特性

ReaderWriterLockSlim提供了一个锁,它允许对资源进行多读者单写者的访问控制。 其目的是在保证线程安全的同时,提高并发性能。

ReaderWriterLockSlim的读写锁特性:

  • 允许多个线程同时读取资源,但在写入时则需要独占访问。
  • 通过不同状态(读取、升级、写入)来控制锁的获取与释放。

ReaderWriterLockSlim的应用场景:

  • 在读多写少的场景中,如缓存数据的读取和更新。
  • 在频繁读取操作的系统中,可以显著提升性能。

3.2.5 lock语句的简便互斥控制

C#的lock语句提供了一种简便的互斥控制机制。 它本质上是使用Monitor的一个包装器,用于简化对象锁的使用。

lock语句的工作原理:

  • lock 语句块中的代码只允许一次被一个线程执行。
  • 它通过使用一个对象作为锁对象,并调用Monitor的Enter和Exit方法。

lock语句的使用场景:

  • 当需要快速实现线程间互斥时使用。
  • 适合实现简单对象的锁定。

3.2.6 volatile关键字与内存可见性

volatile关键字用于指示一个字段可能会被多个线程同时访问,因此编译器和运行时环境需要避免对该字段的某些优化。 这有助于维护内存的可见性。

volatile的作用:

  • 确保对volatile字段的读写操作不会被优化到缓存或寄存器中。
  • 确保对volatile字段的访问立即对其他线程可见。

volatile的应用场景:

  • 当共享变量在多个线程间可见时。
  • 防止编译器和运行时对共享变量的优化,从而确保多线程的正确行为。

3.2.7 Interlocked类提供的原子操作

Interlocked类提供了执行原子操作的方法。 原子操作是指在多线程访问时,其执行是不可中断的,即使中断也不会使操作处于不一致状态。

Interlocked的操作:

  • Interlocked.Increment Interlocked.Decrement :用于原子地增加或减少变量的值。
  • Interlocked.Exchange :用于原子地设置变量的值。
  • Interlocked.CompareExchange :用于比较和交换变量的值。

Interlocked的应用场景:

  • 用于实现计数器、标志位等对线程安全要求极高的操作。
  • 在需要执行无锁操作的场景中,以提高性能。

4. .NET线程池使用

线程池是.NET框架中用于管理线程生命周期的一种高效机制。它提供了一组线程,这些线程可被重复用于执行多个任务。线程池可以大大减少在多线程环境下创建和销毁线程所需的资源开销。在.NET中,线程池由 ThreadPool 类管理,并与工作项(work items)的概念相结合,后者是待执行的任务。由于线程池的这些特点,它在执行短期异步操作时特别有用。

4.1 线程池的基本概念和优势

4.1.1 线程池的工作机制

线程池通过维护一个可重用的工作线程集来工作。当应用程序需要执行新的任务时,它会将任务放入线程池的队列中。线程池中的线程会不断检查这个队列,以获取并执行任务。这一机制的优点在于:

  • 资源复用: 线程池中的线程被设计为可以处理多个请求,从而减少了线程创建和销毁的开销。
  • 性能提升: 减少了上下文切换,因为线程池的线程通常比临时线程更活跃。
  • 简单易用: 开发者不需要手动管理线程的生命周期,只需要将任务提交给线程池即可。

4.1.2 线程池与传统线程的比较

传统线程的管理需要开发者处理线程的创建、执行和销毁。在大量使用线程的场景中,这种模式会产生显著的性能负担。而线程池提供了一种更为高效和简洁的替代方案。以下是使用线程池相比于传统线程的优势:

  • 资源优化: 减少了线程资源的消耗。
  • 错误处理简化: 线程池提供了内置的错误处理机制。
  • 可扩展性: 线程池可以自动调整线程数量以适应当前工作负载。

4.2 线程池的管理与优化

4.2.1 线程池的配置与管理

.NET线程池的配置与管理涉及到理解其默认行为以及如何自定义线程池的参数。线程池的配置包括线程数量、任务队列的最大容量、工作线程的最大存活时间等。下面的代码展示了如何获取和设置一些线程池的参数:

// 获取线程池的一些参数
int workerThreads, completionPortThreads;
ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);

// 设置线程池的参数
ThreadPool.SetMaxThreads(20, 20);

// 设置任务队列容量
// 注意:.NET Core 3.0及之后版本中,SetMaxQueueLength已被弃用
int maxQueueLength = 1000;
ThreadPool.GetMaxQueueLength(out var currentQueueLength);
ThreadPool.SetMaxQueueLength(currentQueueLength); // 在.NET Core 3.0+中,这行代码不会按预期工作

对线程池参数的调整应谨慎进行,因为错误的配置可能会导致性能问题甚至应用崩溃。

4.2.2 线程池的性能优化策略

性能优化策略的关键在于理解工作负载的特点并相应地调整线程池配置。以下是一些常见的优化策略:

  • 避免任务阻塞: 避免提交长时间运行或阻塞线程池线程的任务。
  • 任务分解: 将大任务分解为小任务,以提高并行度和响应性。
  • 使用异步API: 在可能的情况下,使用异步API,让线程池有机会释放线程以处理其他任务。
// 使用异步API示例
public async Task ProcessDataAsync()
{
    await Task.Run(() => 
    {
        // 执行数据处理任务
    });
}

在进行优化时,建议使用性能分析工具来监视应用程序的行为,并根据实际的运行数据调整参数。

在本节中,我们介绍了线程池的基本概念和优势,探讨了如何对线程池进行配置与管理,并提出了一些性能优化策略。理解和运用这些知识,可以帮助开发者写出更加高效和可扩展的多线程应用程序。

5. 异步编程模型(async和await关键字)

5.1 异步编程的基础知识

5.1.1 异步编程的必要性与优势

在现代应用程序中,用户体验至关重要。异步编程允许应用程序在执行长时间运行的任务时保持响应,从而提高用户体验。例如,在Web应用程序中,如果某个操作需要从数据库获取大量数据或处理复杂的计算,使用同步方法可能会使用户界面冻结,直到操作完成。异步方法可以避免这种情况,因为它们允许应用程序在等待长时间操作完成时继续处理其他用户请求。

异步编程还有助于优化服务器资源的使用,特别是在处理I/O密集型和CPU密集型任务时。通过异步处理,可以避免创建不必要的线程和增加上下文切换开销,从而提高应用程序的性能和可伸缩性。

5.1.2 async和await的语法与作用

C#中的 async await 关键字为异步编程提供了简化语法。 async 关键字用于声明一个异步方法,而 await 关键字用于等待一个异步操作的完成,同时不阻塞当前线程。

public async Task MyAsyncMethodAsync()
{
    // 等待一个异步操作
    await SomeAsyncOperation();
    // 继续执行方法中的其他代码
}

在上述代码中, MyAsyncMethodAsync 是一个异步方法,它等待 SomeAsyncOperation 方法的完成。这里的关键是 await 关键字,它告诉编译器在等待期间释放当前方法所使用的资源,并在异步操作完成时重新激活该方法。

参数说明: - async 关键字使得编译器知道该方法包含异步操作,可能需要在等待异步操作完成时挂起。 - await 关键字暂停方法的执行,直到所等待的异步操作完成。在操作完成之前,它不会阻塞当前线程。

逻辑分析: 异步方法通常返回 Task Task<T> 类型,表示异步操作。如果方法不需要返回数据,则返回 Task ;如果需要返回数据,则返回 Task<T> ,其中 T 是返回数据的类型。

异步编程的引入极大地简化了异步代码的编写和理解,使得开发者可以专注于业务逻辑,而不是线程管理的复杂性。然而,异步编程也引入了一些新的挑战,如异步错误处理、死锁预防和性能优化等,这些将在后续章节中深入探讨。

5.2 异步编程的高级应用

5.2.1 异步模式的实现机制

异步模式在.NET中通常是通过返回 Task Task<T> 来实现的。这些类型代表尚未完成的异步操作,它们提供了 ContinueWith 方法,允许开发者在异步操作完成后执行后续操作。但是,这种模式容易导致代码复杂和错误,因为它依赖于回调和任务链。

随着C# 5.0引入的 async await 关键字,异步编程变得更加直观。异步方法使用 async 修饰符声明,并使用 await 操作符等待异步操作完成。编译器会自动处理方法的挂起和恢复,大大简化了异步代码的编写。

5.2.2 异步编程中的错误处理与异常传播

异步编程中的错误处理是一个挑战,因为异常可能在异步操作的任何时刻发生。使用 try-catch 块捕获异常与传统的同步代码有所不同,因为异常可能在异步操作的上下文中抛出。

public async Task AsyncMethodWithExceptionAsync()
{
    try
    {
        // 启动一个异步操作
        var task = SomeOperationAsync();
        // 等待异步操作完成
        await task;
    }
    catch (Exception ex)
    {
        // 处理可能出现的异常
        HandleException(ex);
    }
}

在上面的代码中,如果 SomeOperationAsync 方法抛出异常, await task; 这行代码将引发异常。这个异常将被内部捕获并在 catch 块中处理。需要注意的是,异步方法中的异常不会立即抛出,而是在等待异步操作时抛出。

5.2.3 异步编程的性能优化

尽管异步编程为用户界面的响应性和应用程序的可伸缩性提供了巨大的优势,但不当使用异步方法也可能导致性能问题。例如,过度使用异步方法可能会导致代码过于复杂,从而降低可读性和可维护性。

性能优化的关键在于合理地使用异步操作,避免不必要的上下文切换,以及确保I/O操作和计算密集型任务充分利用异步特性。

public async Task OptimizeAsyncMethodAsync()
{
    // 使用异步I/O操作
    await FileIO.AsyncReadAsync("file.txt");
    // 计算密集型任务应使用Task.Run进行异步处理
    await Task.Run(() => ComplexCalculation());
}

在上述示例中, FileIO.AsyncReadAsync 是一个异步I/O操作,它不会阻塞调用线程,因此不会导致不必要的上下文切换。对于CPU密集型任务,应使用 Task.Run 方法在后台线程上执行,以避免阻塞异步操作。

异步编程是一个深奥的主题,涉及到应用程序的多个方面。通过理解async和await的基础知识和高级应用,开发者可以更好地利用这些工具来构建高性能、响应迅速的应用程序。在后续章节中,我们将探讨如何通过线程通信方法和线程状态管理进一步提升异步编程的效率和可靠性。

6. 线程通信方法(WaitHandle)

6.1 WaitHandle的基本使用

6.1.1 WaitHandle的作用与原理

在多线程环境中,线程之间的协作至关重要。WaitHandle类是.NET中用于线程同步的一组类的基类,它提供了一种方式来通知等待线程某个事件已经发生。WaitHandle的实现包括了AutoResetEvent、ManualResetEvent、Semaphore等,它们可以用于控制对共享资源的访问。

WaitHandle的原理基于事件对象,这些对象能够在不同的线程间同步访问。当一个线程执行到需要等待某个条件成立的点时,它会调用WaitOne()方法,该方法会使线程进入等待状态,直到有另一个线程对相应的WaitHandle对象调用了Set()方法,从而发出信号让等待的线程继续执行。AutoResetEvent和ManualResetEvent的主要区别在于它们重置事件的行为:AutoResetEvent在调用Set()之后,会自动重置事件(即变回未触发状态),而ManualResetEvent则需要手动调用Reset()方法来重置。

6.1.2 AutoResetEvent与ManualResetEvent的使用

AutoResetEvent和ManualResetEvent是WaitHandle的具体实现,它们通过一个布尔标志来控制线程间的同步。下面以示例代码来展示如何使用这两种事件。

// 定义两个事件
AutoResetEvent autoEvent = new AutoResetEvent(false);
ManualResetEvent manualEvent = new ManualResetEvent(false);

// 一个工作线程,它等待事件的发生
void WorkerThread()
{
    Console.WriteLine("WorkerThread waiting.");
    autoEvent.WaitOne(); // 对于AutoResetEvent,这将等待直到事件被设置
    Console.WriteLine("AutoResetEvent triggered.");
    manualEvent.WaitOne(); // 对于ManualResetEvent,这将等待直到事件被设置
    Console.WriteLine("ManualResetEvent triggered.");
}

// 主线程,它触发事件
void MainThread()
{
    Console.WriteLine("MainThread setting AutoResetEvent.");
    autoEvent.Set(); // 设置AutoResetEvent,它会立即重置

    Console.WriteLine("MainThread waiting for WorkerThread.");
    Thread.Sleep(1000); // 模拟一些工作

    Console.WriteLine("MainThread setting ManualResetEvent.");
    manualEvent.Set(); // 设置ManualResetEvent,它不会自动重置
}

// 创建并启动工作线程
Thread worker = new Thread(new ThreadStart(WorkerThread));
worker.Start();

// 启动主线程逻辑
MainThread();

在上面的代码中,我们创建了一个AutoResetEvent和一个ManualResetEvent,然后在一个工作线程中等待这两个事件。主线程负责设置这两个事件。当AutoResetEvent被设置时,等待它的线程会立即被释放,事件随即重置。而ManualResetEvent则会保持触发状态,直到我们显式地调用Reset()方法。

6.2 线程间通信高级技术

6.2.1 Barrier类实现同步屏障

Barrier类用于在多个线程的执行点上实现同步,使得所有线程必须在继续执行前达到这个同步点。它是一个高级同步原语,用于一组线程必须在某个点同时到达并继续执行的场景。

Barrier的一个重要特性是它允许多次同步,也就是说,线程可以到达并离开多个同步点。每个线程到达同步点时,会阻塞直到所有线程都到达,然后Barrier将通知所有线程继续执行,而Barrier本身会被重置,可以再次使用。

// 创建一个同步点,需要4个线程都到达后才能继续执行
Barrier barrier = new Barrier(4, (b) =>
{
    Console.WriteLine($"Barrier hit, all threads arrived: {b.PartiesRemaining}");
});

// 定义一个线程执行的动作
Action action = () =>
{
    for (int i = 0; i < 3; i++)
    {
        Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} at stage {i + 1}.");
        Thread.Sleep(1000); // 模拟工作
        barrier.SignalAndAwait(); // 通知Barrier到达同步点并等待其它线程
    }
};

// 启动4个工作线程
for (int i = 0; i < 4; i++)
{
    new Thread(action).Start();
}

6.2.2 CountDownEvent的计数信号量应用

CountdownEvent是一个同步原语,用于多个线程需要等待某个事件达到特定计数后才能继续执行的场景。它与AutoResetEvent和ManualResetEvent的不同之处在于,CountdownEvent允许初始化一个特定的计数值,而线程需要等待这个计数值到达0时才能通过。

// 初始化一个计数为3的CountdownEvent
CountdownEvent countdown = new CountdownEvent(3);

// 定义一个线程执行的动作
Action action = () =>
{
    Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} completing work.");
    countdown.Signal(); // 完成工作后,减少计数
};

// 启动3个工作线程
for (int i = 0; i < 3; i++)
{
    new Thread(action).Start();
}

// 等待所有线程完成工作
countdown.Wait(); // 阻塞直到计数到达0
Console.WriteLine("All work completed.");

以上代码示例展示了CountdownEvent在多个线程完成任务后同步继续执行的场景。每个工作线程完成自己的任务后调用Signal()方法减少计数,主线程则通过调用Wait()方法等待直到计数达到0,这时所有线程都已完成了任务。

7. 线程状态管理与多线程异常处理

随着多线程编程技术的普及,正确管理线程的状态以及处理异常成为了保证程序稳定运行的关键。本章将深入探讨.NET环境下线程状态的监控与管理、多线程异常处理机制以及线程局部存储和线程优先级的相关知识。

7.1 线程状态的监控与管理

管理线程状态是确保多线程程序正确运行的基础。开发者必须掌握如何监控和控制线程的状态,例如启动、暂停、恢复和终止线程。

7.1.1 线程状态的检查与控制

.NET中的 Thread 类提供了多种方法来检查和控制线程的状态。线程的状态包括但不限于 Running Waiting Suspended AbortRequested Stopped 等。通过 Thread.ThreadState 属性可以获取当前线程的状态。

Thread currentThread = Thread.CurrentThread;
Console.WriteLine($"Current thread state: {currentThread.ThreadState}");

使用 Thread.Sleep 可以让当前线程进入 WaitSleepJoin 状态,从而暂停执行指定的毫秒数。同样, Thread.Suspend Thread.Resume 方法可以用来暂停和恢复线程的执行。但需注意, Suspend Resume 方法已被弃用,因为它们可能会导致死锁。

7.1.2 线程的暂停、恢复与终止操作

线程的暂停与恢复在.NET 4.0之后应使用 Thread.Suspend Thread.Resume 替代方法,通过 Thread 类提供的 Thread.Interrupt 方法可以中断等待状态的线程。

终止线程相对危险,因为不当的线程终止可能导致资源泄露和其他线程的不稳定。推荐使用 Thread.Abort 方法,但应谨慎使用,因为它可能引发 SecurityException 异常。

Thread myThread = new Thread(new ThreadStart(ThreadMethod));
myThread.Start();
// ...其他操作...
myThread.Abort(); // Terminate the thread

7.2 多线程编程中的异常处理机制

在多线程程序中,异常处理尤为复杂。线程内的异常可能不会影响到主线程,或者会传播到其他线程中去。

7.2.1 异常在多线程中的传播与捕获

在.NET中,线程内发生的异常可以通过 ThreadException 事件来捕获,该事件在捕获异常之前,应优先使用 try-catch 语句。

AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    Console.WriteLine($"Unhandled exception: {e.ExceptionObject}");
}

7.2.2 线程安全的异常处理策略

处理多线程异常时,应采用线程安全的策略。最常见的是使用 try-catch-finally 结构,并确保清理线程占用的资源,避免资源泄露。

Thread myThread = new Thread(() =>
{
    try
    {
        // Potentially dangerous code that might throw exceptions
    }
    catch (Exception ex)
    {
        // Handle exceptions for this thread
    }
    finally
    {
        // Ensure resources are released properly
    }
});

7.3 线程局部存储(ThreadLocal )与线程优先级

在多线程中,线程局部存储是避免线程间数据竞争的一种常用技术。而线程的优先级则是影响线程执行顺序的重要因素。

7.3.1 ThreadLocal 的原理与使用场景

ThreadLocal<T> 为每个线程提供了一个独立的变量副本,这意味着每个线程都可以访问自己的局部变量,而不会与其他线程共享。

ThreadLocal<int> _localNumber = new ThreadLocal<int>(() => {
    return new Random().Next(1000);
});

Thread thread1 = new Thread(() => {
    Console.WriteLine($"Thread 1 local number: {_localNumber.Value}");
});

Thread thread2 = new Thread(() => {
    Console.WriteLine($"Thread 2 local number: {_localNumber.Value}");
});

thread1.Start();
thread2.Start();

7.3.2 线程优先级的设置与考虑

线程优先级由 Thread.Priority 属性控制,其值可以是 Highest AboveNormal Normal BelowNormal Lowest 。应谨慎设置线程优先级,因为这可能会影响系统的稳定性和性能。

Thread myThread = new Thread(new ThreadStart(ThreadMethod));
myThread.Priority = ThreadPriority.AboveNormal;
myThread.Start();

7.3.3 优先级反转问题及解决方案

优先级反转是多线程程序中可能遇到的问题,其中低优先级的线程持有一个资源,导致高优先级线程等待,此时可以使用互斥锁、优先级提升等策略解决。

// Example of a simple priority inversion scenario
// Note: This is an illustration, not actual code
Mutex myMutex = new Mutex();
Thread lowPriorityThread = new Thread(new ThreadStart(() =>
{
    // Acquire the mutex
    myMutex.WaitOne();
    // Do some work
    myMutex.ReleaseMutex();
}));

Thread highPriorityThread = new Thread(new ThreadStart(() =>
{
    // Attempt to acquire the mutex
    myMutex.WaitOne();
    // Do some work
    myMutex.ReleaseMutex();
}));

通过这些方法,开发者可以有效地管理线程状态,处理多线程中的异常,并利用线程局部存储和线程优先级来优化应用程序的性能与资源使用。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:C#在.NET框架中提供了强大的多线程编程能力,使得开发者能够利用多核处理器进行并行处理。本资源包含了六个涵盖多线程基础和高级特性的C#.NET实例,如线程创建、线程同步与互斥、线程池使用、线程同步原语、异步编程模型、线程通信、线程状态管理、异常处理、线程局部存储和线程优先级等。开发者通过运行和分析这些实例,可以深入学习和掌握C#.NET多线程编程的实战应用。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值