.NET 多线程编程指南
1. 异步类选择优先级
在 .NET 编程中,选择合适的异步类对于多线程编程至关重要。一般来说,选择异步类的优先级顺序为:Task、ThreadPool 和 Thread。具体解释如下:
-
Task
:优先使用 Task Parallel Library (TPL),它基于 System.Threading.Tasks.Task 类,提供了标准的多线程编程和监控活动,使多线程编程相对简单。Task 是并行循环(Parallel.For() 和 Parallel.ForEach())、PLINQ 等的基础,能实现更复杂的线程场景,如异常处理和任务链/通知。
-
ThreadPool
:如果 TPL 不适用,可考虑使用 ThreadPool。它能有效管理线程创建,通过 CLR 的线程池 System.Threading.ThreadPool 动态决定何时使用现有线程而非创建新线程,提高了线程的复用效率。
-
Thread
:若 ThreadPool 仍无法满足需求,则使用 Thread。
例如,当需要暂停线程时,由于 Task 和 ThreadPool 没有等效方法,可能会使用 Thread.Sleep()。不过,如果不会引入过多不必要的复杂性,可考虑使用定时器代替 Sleep()。
2. 线程池的使用
线程池是提高多线程编程效率的重要工具,但也存在一些需要注意的地方。
-
优点
:线程池能动态决定何时使用现有线程,避免了频繁创建新线程的开销,提高了执行效率。例如,以下代码展示了如何使用线程池:
using System;
using System.Threading;
public class Program
{
public const int Repetitions = 1000;
public static void Main()
{
for (int count = 0; count < Repetitions; count++)
{
ThreadPool.QueueUserWorkItem(DoWork, '.');
Console.Write('-');
}
// Pause until the thread completes
}
public static void DoWork(object state)
{
for (int count = 0; count < Repetitions; count++)
{
Console.Write(state);
}
}
}
该代码的输出是
.
和
-
的混合,通过复用线程,在单处理器和多处理器计算机上都能实现更高效的执行。
-
缺点
:
-
线程耗尽
:I/O 操作和其他内部使用线程池的框架方法可能会消耗线程,当线程池中的所有线程都被消耗时,会导致执行延迟,极端情况下可能会造成死锁。
-
缺乏控制
:ThreadPool API 不返回线程或任务的句柄,调用线程无法使用线程管理函数对其进行控制,若不添加自定义实现,也无法监控线程状态。
3. 应用程序域中的未处理异常
在多线程编程中,处理未处理异常是一个重要问题。当第三方组件创建的线程或线程池中的排队工作抛出未处理异常时,Main() 中的 try/catch 块无法捕获这些异常。为了在出现未处理异常时保存工作数据和记录异常信息,可通过应用程序域的 UnhandledException 事件进行注册。示例代码如下:
using System;
using System.Threading;
public class Program
{
public static void Main()
{
try
{
// Register a callback to receive notifications of any unhandled exception.
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
ThreadPool.QueueUserWorkItem(
state =>
{
throw new Exception(
"Arbitrary Exception");
});
// ...
// Wait for the unhandled exception to fire
// ADVANCED: Use ManualResetEvent to avoid timing dependent code.
Thread.Sleep(10000);
Console.WriteLine("Still running...");
}
finally
{
Console.WriteLine("Exiting...");
}
}
public static void ThrowException()
{
throw new ApplicationException(
"Arbitrary exception");
}
static void OnUnhandledException(
object sender,
UnhandledExceptionEventArgs eventArgs)
{
Exception exception =
(Exception)eventArgs.ExceptionObject;
Console.WriteLine("ERROR ({0}):{1} ---> {2}",
exception.GetType().Name,
exception.Message,
exception.InnerException.Message);
}
}
UnhandledException 回调会在应用程序域内的所有线程(包括主线程)抛出未处理异常时触发,但它只是一个通知机制,无法捕获和处理异常以让应用程序继续运行。事件触发后,应用程序将退出。
4. 多线程同步问题
多线程编程的难点之一是识别多个线程可能同时访问的数据,并对这些数据进行同步,以防止同时访问导致的数据完整性问题。
-
未同步状态示例
:以下代码展示了未同步状态下的多线程问题:
using System;
using System.Threading.Tasks;
class Program
{
const int _Total = int.MaxValue;
static long _Count = 0;
public static void Main()
{
Task task = Task.Factory.StartNew(Decrement);
// Increment
for (int i = 0; i < _Total; i++)
{
_Count++;
}
task.Wait();
Console.WriteLine("Count = {0}", _Count);
}
static void Decrement()
{
// Decrement
for (int i = 0; i < _Total; i++)
{
_Count--;
}
}
}
该代码的输出通常不是 0,因为
_Count++
和
_Count--
语句的各个步骤可能会相互交织,导致竞态条件。以下是一个示例执行过程:
| 主线程 | 递减线程 | Count |
| — | — | — |
| 从 _Count 中复制值 0 | | 0 |
| 将复制的值(0)加 1,结果为 1 | | 0 |
| 将结果值(1)复制到 _Count | | 1 |
| 从 _Count 中复制值 1 | 从 _Count 中复制值 1 | 1 |
| 将复制的值(1)加 1,结果为 2 | | 1 |
| 将结果值(2)复制到 _Count | | 2 |
| | 将复制的值(1)减 1,结果为 0 | 2 |
| | 将结果值(0)复制到 _Count | 0 |
-
局部变量的并发问题
:虽然局部变量默认不共享,但如果代码将局部变量暴露给多个线程,也会引发并发问题。例如,以下代码中的局部变量
x在并行循环中被多个线程同时修改,导致竞态条件:
using System;
using System.Threading.Tasks;
class Program
{
public static void Main()
{
int x = 0;
Parallel.For(0, int.MaxValue, i =>
{
x++;
x--;
});
Console.WriteLine("Count = {0}", x);
}
}
5. 使用 Monitor 进行同步
为了解决多线程同步问题,可以使用 System.Threading.Monitor 类。通过调用 Monitor.Enter() 和 Monitor.Exit() 方法,可以标记受保护的代码段,确保同一时间只有一个线程可以执行该代码段。以下是使用 Monitor 进行同步的示例代码:
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
const int _Total = int.MaxValue;
static long _Count = 0;
readonly static object _Sync = new object();
public static void Main()
{
Task task = Task.Factory.StartNew(Decrement);
// Increment
for (int i = 0; i < _Total; i++)
{
bool lockTaken = false;
Monitor.Enter(_Sync, ref lockTaken);
try
{
_Count++;
}
finally
{
if (lockTaken)
{
Monitor.Exit(_Sync);
}
}
}
task.Wait();
Console.WriteLine("Count = {0}", _Count);
}
static void Decrement()
{
for (int i = 0; i < _Total; i++)
{
bool lockTaken = false;
Monitor.Enter(_Sync, ref lockTaken);
try
{
_Count--;
}
finally
{
if (lockTaken)
{
Monitor.Exit(_Sync);
}
}
}
}
}
该代码的输出为
Count = 0
,表明通过 Monitor 同步,避免了竞态条件。
需要注意的是,在 .NET 4.0 中,Monitor.Enter() 方法添加了 lockTaken 参数,用于可靠地捕获异常。在之前的版本中,由于缺乏该参数,可能会导致锁泄漏和死锁问题。
此外,Monitor 还支持 Pulse() 方法,用于同步生产者 - 消费者模式。生产者线程调用 Monitor.Pulse() 通知消费者线程有可用的项目,消费者线程在生产者线程调用 Monitor.Exit() 后获取锁并开始处理项目,确保了生产和消费的顺序。
.NET 多线程编程指南(下半部分)
6. 更多同步类型及方法
除了 Monitor 之外,还有其他一些同步类型和方法可用于多线程编程,下面为你详细介绍:
-
Lock 关键字
:Lock 关键字是 Monitor 的语法糖,使用起来更加简洁。它本质上是对 Monitor.Enter() 和 Monitor.Exit() 的封装。示例代码如下:
using System;
using System.Threading.Tasks;
class Program
{
const int _Total = int.MaxValue;
static long _Count = 0;
readonly static object _Sync = new object();
public static void Main()
{
Task task = Task.Factory.StartNew(Decrement);
// Increment
for (int i = 0; i < _Total; i++)
{
lock (_Sync)
{
_Count++;
}
}
task.Wait();
Console.WriteLine("Count = {0}", _Count);
}
static void Decrement()
{
for (int i = 0; i < _Total; i++)
{
lock (_Sync)
{
_Count--;
}
}
}
}
- Volatile :Volatile 关键字用于确保字段的读写操作直接在内存中进行,而不是使用 CPU 缓存。这可以避免因缓存不一致导致的问题。例如:
class Program
{
private static volatile bool _isRunning = true;
public static void Main()
{
Task.Run(() =>
{
while (_isRunning)
{
// Do some work
}
});
// Stop the task after some time
_isRunning = false;
}
}
- Mutex :Mutex(互斥体)是一种跨进程的同步原语,用于确保同一时间只有一个线程或进程可以访问共享资源。示例代码如下:
using System;
using System.Threading;
class Program
{
private static Mutex _mutex = new Mutex();
public static void Main()
{
_mutex.WaitOne();
try
{
// Access the shared resource
}
finally
{
_mutex.ReleaseMutex();
}
}
}
-
WaitHandle
:WaitHandle 是一个抽象基类,用于等待一个或多个等待句柄变为有信号状态。常见的子类有 ManualResetEvent 和 AutoResetEvent。
- ManualResetEvent :可以手动设置和重置信号状态。示例代码如下:
using System;
using System.Threading;
class Program
{
private static ManualResetEvent _mre = new ManualResetEvent(false);
public static void Main()
{
Task.Run(() =>
{
_mre.WaitOne();
// Do some work after the signal
});
// Set the signal after some time
_mre.Set();
}
}
- **AutoResetEvent**:在等待线程收到信号后会自动重置信号状态。示例代码如下:
using System;
using System.Threading;
class Program
{
private static AutoResetEvent _are = new AutoResetEvent(false);
public static void Main()
{
Task.Run(() =>
{
_are.WaitOne();
// Do some work after the signal
});
// Set the signal after some time
_are.Set();
}
}
7. 多线程模式
在多线程编程中,有一些常见的模式可以帮助我们更好地组织和管理线程,以下是几种常见的模式:
-
生产者 - 消费者模式
:生产者线程负责生产数据,消费者线程负责消费数据。可以使用 Monitor 的 Pulse() 和 Exit() 方法来实现生产和消费的同步。示例代码如下:
using System;
using System.Collections.Generic;
using System.Threading;
class Program
{
private static Queue<int> _queue = new Queue<int>();
private static readonly object _lock = new object();
private static bool _isProducing = true;
public static void Main()
{
Task.Run(Producer);
Task.Run(Consumer);
// Stop producing after some time
Thread.Sleep(5000);
_isProducing = false;
}
static void Producer()
{
while (_isProducing)
{
lock (_lock)
{
_queue.Enqueue(new Random().Next(100));
Monitor.Pulse(_lock);
}
Thread.Sleep(100);
}
}
static void Consumer()
{
while (true)
{
lock (_lock)
{
while (_queue.Count == 0)
{
Monitor.Wait(_lock);
}
int item = _queue.Dequeue();
Console.WriteLine("Consumed: " + item);
}
}
}
}
- 线程本地存储(Thread Local Storage) :线程本地存储允许每个线程拥有自己独立的变量副本。可以使用 ThreadLocal 类来实现。示例代码如下:
using System;
using System.Threading;
class Program
{
private static ThreadLocal<int> _threadLocal = new ThreadLocal<int>(() => 0);
public static void Main()
{
Task.Run(() =>
{
_threadLocal.Value++;
Console.WriteLine("Thread 1: " + _threadLocal.Value);
});
Task.Run(() =>
{
_threadLocal.Value++;
Console.WriteLine("Thread 2: " + _threadLocal.Value);
});
}
}
- 定时器(Timers) :定时器可以在指定的时间间隔内执行任务。在 .NET 中有多种定时器可供选择,如 System.Timers.Timer、System.Threading.Timer 和 System.Windows.Forms.Timer。以下是 System.Threading.Timer 的示例代码:
using System;
using System.Threading;
class Program
{
private static Timer _timer;
public static void Main()
{
_timer = new Timer(Callback, null, 0, 1000);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
_timer.Dispose();
}
static void Callback(object state)
{
Console.WriteLine("Timer ticked at: " + DateTime.Now);
}
}
8. 异步编程模型
异步编程模型可以提高程序的响应性和性能,尤其是在处理 I/O 密集型任务时。在 .NET 中,可以使用 async 和 await 关键字来实现异步编程。示例代码如下:
using System;
using System.Threading.Tasks;
class Program
{
public static async Task Main()
{
Task<int> task = LongRunningTask();
// Do some other work while the task is running
Console.WriteLine("Doing other work...");
int result = await task;
Console.WriteLine("Task result: " + result);
}
static async Task<int> LongRunningTask()
{
await Task.Delay(2000);
return 42;
}
}
9. 背景工作者模式
背景工作者模式用于在后台线程中执行耗时的任务,同时保持 UI 的响应性。在 Windows 应用程序中,可以使用 BackgroundWorker 类。示例代码如下:
using System;
using System.ComponentModel;
using System.Windows.Forms;
namespace BackgroundWorkerExample
{
public partial class Form1 : Form
{
private BackgroundWorker _backgroundWorker;
public Form1()
{
InitializeComponent();
_backgroundWorker = new BackgroundWorker();
_backgroundWorker.DoWork += BackgroundWorker_DoWork;
_backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
}
private void button1_Click(object sender, EventArgs e)
{
_backgroundWorker.RunWorkerAsync();
}
private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
// Do some long-running work
Thread.Sleep(5000);
}
private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show("Task completed!");
}
}
}
10. Windows UI 编程中的多线程
在 Windows UI 编程中,由于 UI 线程负责绘制和响应用户交互,因此耗时的任务应该在后台线程中执行,以避免 UI 冻结。可以使用 Dispatcher 来更新 UI 元素。示例代码如下:
using System;
using System.Threading.Tasks;
using System.Windows;
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private async void Button_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
// Do some long-running work
Thread.Sleep(5000);
// Update the UI on the UI thread
this.Dispatcher.Invoke(() =>
{
MessageBox.Show("Task completed!");
});
});
}
}
}
11. 总结
在 .NET 多线程编程中,我们可以根据不同的需求选择合适的异步类,如 Task、ThreadPool 和 Thread。同时,要注意处理未处理异常,避免因异常导致程序崩溃。在多线程同步方面,有多种同步类型和方法可供选择,如 Monitor、Lock、Volatile、Mutex 等。此外,还可以使用各种多线程模式和异步编程模型来提高程序的性能和响应性。
在实际开发中,要根据具体的场景选择合适的同步方法和模式,避免死锁和竞态条件的发生。同时,要注意线程安全,确保共享数据的完整性。希望通过本文的介绍,你能对 .NET 多线程编程有更深入的理解和掌握。
超级会员免费看
8

被折叠的 条评论
为什么被折叠?



