在C#中,多线程开发和线程间通信是非常重要的技术,特别是在需要并发处理任务的应用中。以下是一些实现多线程开发、线程间通信的常用方法:
1. 使用 lock 关键字
lock 关键字可以用来确保某个代码块在同一时间只能被一个线程执行,以避免多个线程同时访问共享资源时产生冲突。
private readonly object _lockObject = new object();
public void ThreadSafeMethod()
{
lock (_lockObject)
{
// 这里的代码块在任意时间点只能有一个线程执行
}
}
2. 使用 Monitor 类
Monitor 类提供了更高级的线程同步功能,可以用来实现等待和脉冲信号。
private readonly object _monitorObject = new object();
public void ThreadSafeMethod()
{
Monitor.Enter(_monitorObject);
try
{
// 线程安全的代码块
}
finally
{
Monitor.Exit(_monitorObject);
}
}
public void WaitMethod()
{
lock (_monitorObject)
{
Monitor.Wait(_monitorObject);
}
}
public void PulseMethod()
{
lock (_monitorObject)
{
Monitor.Pulse(_monitorObject);
}
}
Monitor 类提供了更高级的线程同步功能,允许线程在某个对象上进行锁定、等待、脉冲信号等操作。与 lock 关键字相比,Monitor 提供了更细粒度的控制,特别是 Wait 和 Pulse 方法,用于实现线程间的更复杂的通信机制。
下面是一个使用 Monitor 类的示例,该示例演示了一个简单的生产者-消费者模型,其中生产者向队列中添加数据,消费者从队列中读取数据。通过 Monitor.Wait 和 Monitor.Pulse 方法,生产者可以通知消费者有新数据,消费者可以在数据为空时等待。
Monitor 示例
using System;
using System.Collections.Generic;
using System.Threading;
class Program
{
private static readonly object _monitorObject = new object();
private static Queue<int> _queue = new Queue<int>();
private static bool _isProducing = true;
static void Main()
{
Thread producer = new Thread(Producer);
Thread consumer = new Thread(Consumer);
producer.Start();
consumer.Start();
producer.Join();
consumer.Join();
Console.WriteLine("生产者和消费者都已完成");
}
static void Producer()
{
for (int i = 0; i < 10; i++)
{
lock (_monitorObject)
{
_queue.Enqueue(i);
Console.WriteLine($"Produced: {i}");
Monitor.Pulse(_monitorObject); // 通知等待的线程有新数据
}
Thread.Sleep(100); // 模拟生产延迟
}
lock (_monitorObject)
{
_isProducing = false;
Monitor.Pulse(_monitorObject); // 通知等待的线程生产已经结束
}
}
static void Consumer()
{
while (true)
{
int item;
lock (_monitorObject)
{
while (_queue.Count == 0)
{
if (!_isProducing)
{
return; // 生产已经结束且队列为空时,退出消费者线程
}
Monitor.Wait(_monitorObject); // 等待生产者的信号
}
item = _queue.Dequeue();
}
Console.WriteLine($"Consumed: {item}");
}
}
}
3. 使用 AutoResetEvent 和 ManualResetEvent
这些类提供了线程间的信号机制,可以让一个线程等待另一个线程的通知。
private static AutoResetEvent _autoResetEvent = new AutoResetEvent(false);
public void WaitingThread()
{
Console.WriteLine("等待信号...");
_autoResetEvent.WaitOne();
Console.WriteLine("收到信号,继续执行...");
}
public void SignalingThread()
{
Console.WriteLine("发送信号...");
_autoResetEvent.Set();
}
在C#中,AutoResetEvent 和 ManualResetEvent 是两种常用的用于线程间通信的同步机制。这两者都派生自 EventWaitHandle 类,并且都可以用来发出和等待信号,但它们在行为上有所不同:
AutoResetEvent:当一个等待线程被唤醒后,信号会自动重置。换句话说,每次调用 Set 方法只会唤醒一个等待的线程。
ManualResetEvent:信号在被设置后会保持,直到显式调用 Reset 方法。这样,所有等待的线程都会被唤醒,直到信号被重置。
AutoResetEvent 示例
using System;
using System.Threading;
class Program
{
private static AutoResetEvent _autoResetEvent = new AutoResetEvent(false);
static void Main()
{
Thread t1 = new Thread(WaitingThread);
t1.Start();
Thread.Sleep(1000); // 模拟主线程的一些工作
Console.WriteLine("主线程:发送信号...");
_autoResetEvent.Set(); // 唤醒等待的线程
t1.Join();
Console.WriteLine("主线程:结束");
}
static void WaitingThread()
{
Console.WriteLine("等待线程:等待信号...");
_autoResetEvent.WaitOne(); // 等待信号
Console.WriteLine("等待线程:收到信号,继续执行...");
}
}
ManualResetEvent 示例
using System;
using System.Threading;
class Program
{
private static ManualResetEvent _manualResetEvent = new ManualResetEvent(false);
static void Main()
{
Thread t1 = new Thread(WaitingThread);
Thread t2 = new Thread(WaitingThread);
t1.Start();
t2.Start();
Thread.Sleep(1000); // 模拟主线程的一些工作
Console.WriteLine("主线程:发送信号...");
_manualResetEvent.Set(); // 唤醒所有等待的线程
t1.Join();
t2.Join();
Console.WriteLine("主线程:重置信号...");
_manualResetEvent.Reset(); // 重置信号
Console.WriteLine("主线程:结束");
}
static void WaitingThread()
{
Console.WriteLine("等待线程:等待信号...");
_manualResetEvent.WaitOne(); // 等待信号
Console.WriteLine("等待线程:收到信号,继续执行...");
}
}
结合 AutoResetEvent 和 lock 实现线程安全的生产者-消费者模型示例
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
class Program
{
private static AutoResetEvent _autoResetEvent = new AutoResetEvent(false);
private static ConcurrentQueue<int> _queue = new ConcurrentQueue<int>();
private static readonly object _lockObject = new object();
static void Main()
{
Task producer = Task.Run(() => Producer());
Task consumer = Task.Run(() => Consumer());
Task.WaitAll(producer, consumer);
Console.WriteLine("生产者和消费者都已完成");
}
static void Producer()
{
for (int i = 0; i < 10; i++)
{
lock (_lockObject)
{
_queue.Enqueue(i);
Console.WriteLine($"Produced: {i}");
_autoResetEvent.Set(); // 发送信号,通知消费者有新数据
}
Thread.Sleep(100); // 模拟生产延迟
}
}
static void Consumer()
{
while (true)
{
_autoResetEvent.WaitOne(); // 等待生产者的信号
lock (_lockObject)
{
while (_queue.TryDequeue(out int item))
{
Console.WriteLine($"Consumed: {item}");
}
}
// 退出条件
if (_queue.IsEmpty)
{
break;
}
}
}
}
关键点说明
AutoResetEvent:
每次调用 Set 方法,只会唤醒一个等待的线程,之后信号会自动重置。
在本例中,只有一个等待线程,所以 Set 后会立即唤醒。
ManualResetEvent:
调用 Set 方法后,信号会保持设置状态,唤醒所有等待的线程。
需要调用 Reset 方法来手动重置信号状态。
在本例中,两个线程都会在 Set 被调用后被唤醒。
4. 使用 Semaphore 和 SemaphoreSlim
Semaphore 和 SemaphoreSlim 允许指定多个线程同时访问共享资源。
private static SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1); // 初始计数为1,最大计数为1
public async Task ThreadSafeMethod()
{
await _semaphoreSlim.WaitAsync(); // 异步请求信号量
try
{
// 线程安全的代码块
}
finally
{
_semaphoreSlim.Release(); // 释放信号量
}
}
Semaphore 和 SemaphoreSlim 是用于控制对资源访问的同步机制,它们允许多个线程同时访问一定数量的资源。SemaphoreSlim 是 Semaphore 的轻量级版本,适用于单进程内的同步,性能更高。以下是使用 Semaphore 和 SemaphoreSlim 的示例。
Semaphore 示例
using System;
using System.Threading;
class Program
{
// 初始化一个信号量,初始计数为3,最大计数为3
private static Semaphore _semaphore = new Semaphore(3, 3);
static void Main()
{
for (int i = 0; i < 10; i++)
{
Thread thread = new Thread(Worker);
thread.Start(i);
}
}
static void Worker(object id)
{
Console.WriteLine($"Thread {id} is waiting to enter the semaphore.");
_semaphore.WaitOne(); // 请求信号量
Console.WriteLine($"Thread {id} has entered the semaphore.");
// 模拟工作
Thread.Sleep(1000);
Console.WriteLine($"Thread {id} is leaving the semaphore.");
_semaphore.Release(); // 释放信号量
}
}
在这个示例中,有10个线程尝试进入信号量,但同时最多只能有3个线程进入信号量。其余线程需要等待,直到有线程离开信号量。
SemaphoreSlim 示例
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
// 初始化一个信号量,初始计数为3,最大计数为3
private static SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(3, 3);
static async Task Main()
{
Task[] tasks = new Task[10];
for (int i = 0; i < 10; i++)
{
int taskId = i; // 为了避免闭包问题
tasks[i] = Task.Run(() => Worker(taskId));
}
await Task.WhenAll(tasks);
Console.WriteLine("所有任务完成");
}
static async Task Worker(int id)
{
Console.WriteLine($"Task {id} is waiting to enter the semaphore.");
await _semaphoreSlim.WaitAsync(); // 异步请求信号量
Console.WriteLine($"Task {id} has entered the semaphore.");
// 模拟工作
await Task.Delay(1000);
Console.WriteLine($"Task {id} is leaving the semaphore.");
_semaphoreSlim.Release(); // 释放信号量
}
}
关键点说明
初始化信号量:
Semaphore 和 SemaphoreSlim 都需要指定初始计数和最大计数。初始计数表示当前可用的资源数量,最大计数表示资源的总数量。
请求信号量:
Semaphore.WaitOne() 和 SemaphoreSlim.WaitAsync() 用于请求信号量。如果信号量计数大于0,则减小计数并立即返回。如果计数为0,则阻塞直到计数大于0。
释放信号量:
Semaphore.Release() 和 SemaphoreSlim.Release() 用于释放信号量,增加信号量的计数,并唤醒等待的线程。
异步支持:
SemaphoreSlim 支持异步方法,可以使用 WaitAsync 方法异步等待信号量,适用于需要高并发的异步编程场景。
5. 使用 ReaderWriterLockSlim
ReaderWriterLockSlim 提供了一种允许多个线程同时读取但只有一个线程写入的锁机制。
private static ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
public void ReadMethod()
{
_rwLock.EnterReadLock();
try
{
// 线程安全的读取操作
}
finally
{
_rwLock.ExitReadLock();
}
}
public void WriteMethod()
{
_rwLock.EnterWriteLock();
try
{
// 线程安全的写入操作
}
finally
{
_rwLock.ExitWriteLock();
}
}
ReaderWriterLockSlim 是一种高效的锁机制,允许多个线程同时读取资源,但在写入资源时只允许一个线程访问。它提供了细粒度的控制,适用于读多写少的场景。
下面是一个使用 ReaderWriterLockSlim 的示例,展示如何在多线程环境中进行线程安全的读取和写入操作。
ReaderWriterLockSlim 示例
using System;
using System.Collections.Generic;
using System.Threading;
class Program
{
private static ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
private static List<int> _items = new List<int>();
static void Main()
{
Thread writer = new Thread(Writer);
writer.Start();
Thread[] readers = new Thread[5];
for (int i = 0; i < 5; i++)
{
readers[i] = new Thread(Reader);
readers[i].Start(i);
}
writer.Join();
foreach (var reader in readers)
{
reader.Join();
}
Console.WriteLine("所有线程已完成");
}
static void Writer()
{
for (int i = 0; i < 10; i++)
{
_rwLock.EnterWriteLock();
try
{
_items.Add(i);
Console.WriteLine($"Writer added: {i}");
}
finally
{
_rwLock.ExitWriteLock();
}
Thread.Sleep(500); // 模拟写操作的延迟
}
}
static void Reader(object readerId)
{
while (true)
{
_rwLock.EnterReadLock();
try
{
if (_items.Count == 10)
{
break; // 读取完成,退出循环
}
foreach (var item in _items)
{
Console.WriteLine($"Reader {readerId} read: {item}");
}
}
finally
{
_rwLock.ExitReadLock();
}
Thread.Sleep(100); // 模拟读取操作的延迟
}
}
}
6. 使用 ConcurrentQueue 和 BlockingCollection
这些集合类提供了线程安全的数据结构,用于线程间的消息传递。
private BlockingCollection<int> _blockingCollection = new BlockingCollection<int>();
public void Producer()
{
for (int i = 0; i < 10; i++)
{
_blockingCollection.Add(i);
Console.WriteLine($"Produced: {i}");
}
_blockingCollection.CompleteAdding();
}
public void Consumer()
{
foreach (var item in _blockingCollection.GetConsumingEnumerable())
{
Console.WriteLine($"Consumed: {item}");
}
}
ConcurrentQueue 和 BlockingCollection 都是.NET Framework 中用于线程安全的集合类,特别适用于多线程环境下的生产者-消费者模式。ConcurrentQueue 是一个线程安全的先进先出 (FIFO) 队列,可以被多个线程安全地同时访问。BlockingCollection 是一个实现了生产者-消费者模式的线程安全集合类,它包装了一个 ConcurrentQueue,并提供了阻塞式的添加和取出元素的方法。
ConcurrentQueue 示例
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
class Program
{
private static ConcurrentQueue<int> _concurrentQueue = new ConcurrentQueue<int>();
static void Main()
{
Task producer = Task.Run(() => Producer());
Task consumer = Task.Run(() => Consumer());
Task.WaitAll(producer, consumer);
Console.WriteLine("生产者和消费者都已完成");
}
static void Producer()
{
for (int i = 0; i < 10; i++)
{
_concurrentQueue.Enqueue(i);
Console.WriteLine($"Produced: {i}");
Thread.Sleep(100); // 模拟生产延迟
}
}
static void Consumer()
{
while (!_concurrentQueue.IsEmpty)
{
if (_concurrentQueue.TryDequeue(out int item))
{
Console.WriteLine($"Consumed: {item}");
}
else
{
Thread.Sleep(50); // 模拟消费者等待
}
}
}
}
关键点说明
初始化 ConcurrentQueue:
private static ConcurrentQueue<int> _concurrentQueue = new ConcurrentQueue<int>();
创建一个 ConcurrentQueue 实例,用于存储整数类型的数据。
生产者方法:
使用 Enqueue 方法向队列中添加数据,它是线程安全的,可以被多个线程同时调用。
static void Producer()
{
for (int i = 0; i < 10; i++)
{
_concurrentQueue.Enqueue(i);
Console.WriteLine($"Produced: {i}");
Thread.Sleep(100); // 模拟生产延迟
}
}
消费者方法:
使用 TryDequeue 方法从队列中取出数据,它也是线程安全的。
如果队列为空,TryDequeue 将返回 false,消费者线程可以做一些等待或其他处理。
static void Consumer()
{
while (!_concurrentQueue.IsEmpty)
{
if (_concurrentQueue.TryDequeue(out int item))
{
Console.WriteLine($"Consumed: {item}");
}
else
{
Thread.Sleep(50); // 模拟消费者等待
}
}
}
BlockingCollection 示例
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
class Program
{
private static BlockingCollection<int> _blockingCollection = new BlockingCollection<int>();
static void Main()
{
Task producer = Task.Run(() => Producer());
Task consumer = Task.Run(() => Consumer());
Task.WaitAll(producer, consumer);
Console.WriteLine("生产者和消费者都已完成");
}
static void Producer()
{
for (int i = 0; i < 10; i++)
{
_blockingCollection.Add(i);
Console.WriteLine($"Produced: {i}");
Thread.Sleep(100); // 模拟生产延迟
}
_blockingCollection.CompleteAdding(); // 标记生产结束
}
static void Consumer()
{
foreach (var item in _blockingCollection.GetConsumingEnumerable())
{
Console.WriteLine($"Consumed: {item}");
Thread.Sleep(200); // 模拟消费延迟
}
}
}
关键点说明
初始化 BlockingCollection:
private static BlockingCollection<int> _blockingCollection = new BlockingCollection<int>();
创建一个 BlockingCollection 实例,用于存储整数类型的数据。
生产者方法:
使用 Add 方法向集合中添加数据,如果集合已满则阻塞线程,直到有空间可以添加数据或者集合被标记为完成添加。
使用 CompleteAdding 方法来标记生产结束。
static void Producer()
{
for (int i = 0; i < 10; i++)
{
_blockingCollection.Add(i);
Console.WriteLine($"Produced: {i}");
Thread.Sleep(100); // 模拟生产延迟
}
_blockingCollection.CompleteAdding(); // 标记生产结束
}
消费者方法:
使用 GetConsumingEnumerable 方法获取一个可以消费的迭代器,它会阻塞线程,直到集合中有数据可以消费或者集合完成添加并且所有数据都已被消费。
static void Consumer()
{
foreach (var item in _blockingCollection.GetConsumingEnumerable())
{
Console.WriteLine($"Consumed: {item}");
Thread.Sleep(200); // 模拟消费延迟
}
}
7. 使用 Task 和 TaskCompletionSource
在使用异步编程模型时,TaskCompletionSource 可以用于在任务之间传递结果和状态。
public async Task<int> RunTask()
{
var tcs = new TaskCompletionSource<int>();
Task.Run(() =>
{
Thread.Sleep(1000); // 模拟工作
tcs.SetResult(42);
});
return await tcs.Task;
}
public async Task ExecuteAsync()
{
int result = await RunTask();
Console.WriteLine($"Result: {result}");
}
8.总结
以上几种方法都是C#中常用的多线程技术,具体选择哪种方法需要根据具体的应用场景来决定。