线程同步是多线程编程中确保共享资源安全访问的关键机制,C#提供lock、Monitor、Mutex、SemaphoreSlim、ReaderWriterLockSlim、Interlocked等工具,以及并发集合和Channel等现代模式,用于解决竞态条件、数据不一致等问题,选择合适机制需权衡场景、性能与复杂度。

C#中的线程同步,说白了,就是为了让多个线程在访问共享资源时,能“排队”或者“协商”,避免互相干扰,导致数据混乱或者程序崩溃。想象一下,如果多个人同时去一个银行柜台取钱,但只有一个柜员,大家又不按顺序来,那场面肯定会乱套,账目也会出错。线程同步就是那个维持秩序的机制,确保每次只有一个线程能够安全地操作那份共享的“钱”。它在多线程编程中至关重要,是保证程序正确性和数据完整性的基石。
在C#中实现线程同步,我们有一系列工具和策略,从最基础的锁机制到更高级的并发集合,每种都有其适用场景和考量。
1. lock
这是C#中最简单、最常用的同步机制。它确保在给定时间内,只有一个线程可以进入被锁定的代码块。
public class Counter
{
private int _count = 0;
private readonly object _lockObject = new object(); // 用于锁定的私有对象
public void Increment()
{
lock (_lockObject) // 锁定_lockObject,确保同一时间只有一个线程能执行这里的代码
{
_count++;
Console.WriteLine($"Incremented to: {_count}");
}
}
public int GetCount()
{
lock (_lockObject) // 读取时也需要锁定,防止在读取过程中_count被其他线程修改
{
return _count;
}
}
}我个人觉得,对于大多数简单的共享资源访问,
lock
2. Monitor
Monitor
lock
lock
Monitor.Enter
Monitor.Exit
try/finally
Monitor
Wait
Pulse
PulseAll
public class ProducerConsumer
{
private readonly Queue<int> _queue = new Queue<int>();
private readonly object _lockObject = new object();
private const int Capacity = 5;
public void Produce(int item)
{
lock (_lockObject)
{
while (_queue.Count == Capacity)
{
Console.WriteLine("Queue is full. Producer waiting...");
Monitor.Wait(_lockObject); // 队列满时,生产者等待
}
_queue.Enqueue(item);
Console.WriteLine($"Produced: {item}. Queue size: {_queue.Count}");
Monitor.PulseAll(_lockObject); // 通知所有等待的消费者
}
}
public int Consume()
{
lock (_lockObject)
{
while (_queue.Count == 0)
{
Console.WriteLine("Queue is empty. Consumer waiting...");
Monitor.Wait(_lockObject); // 队列空时,消费者等待
}
int item = _queue.Dequeue();
Console.WriteLine($"Consumed: {item}. Queue size: {_queue.Count}");
Monitor.PulseAll(_lockObject); // 通知所有等待的生产者
return item;
}
}
}当我们需要实现生产者-消费者模式或者更复杂的线程间通信时,
Monitor
Wait
Pulse
3. Mutex
Mutex
lock
Monitor
Mutex
// 示例:确保应用程序单实例运行
public class SingleInstanceApp
{
private static Mutex _appMutex;
public static bool IsSingleInstance()
{
// 尝试创建或打开一个命名Mutex
_appMutex = new Mutex(true, "MyUniqueApplicationMutexName", out bool createdNew);
return createdNew; // 如果createdNew为true,表示当前是第一个实例
}
public static void ReleaseMutex()
{
_appMutex?.ReleaseMutex();
_appMutex?.Dispose();
}
}对于需要跨进程同步的场景,
Mutex
lock
4. Semaphore
SemaphoreSlim
信号量用于限制同时访问某个资源的线程数量。例如,你可能有一个数据库连接池,只允许最多10个线程同时获取连接。
SemaphoreSlim
Semaphore
public class ResourcePool
{
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(3); // 允许3个线程同时访问
private int _resourceCounter = 0;
public async Task AccessResourceAsync()
{
await _semaphore.WaitAsync(); // 等待获取信号量
try
{
int currentAccessId = Interlocked.Increment(ref _resourceCounter);
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} acquired resource. Access ID: {currentAccessId}");
await Task.Delay(1000); // 模拟资源访问耗时
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} finished resource access. Access ID: {currentAccessId}");
}
finally
{
_semaphore.Release(); // 释放信号量
}
}
}在我看来,
SemaphoreSlim
5. ReaderWriterLockSlim
当一个资源被频繁读取但很少写入时,传统的互斥锁会降低并发性(因为读操作之间也会互相阻塞)。
ReaderWriterLockSlim
public class CachedData
{
private readonly Dictionary<string, string> _cache = new Dictionary<string, string>();
private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
public string GetValue(string key)
{
_rwLock.EnterReadLock(); // 进入读模式
try
{
return _cache.ContainsKey(key) ? _cache[key] : null;
}
finally
{
_rwLock.ExitReadLock(); // 退出读模式
}
}
public void SetValue(string key, string value)
{
_rwLock.EnterWriteLock(); // 进入写模式
try
{
_cache[key] = value;
}
finally
{
_rwLock.ExitWriteLock(); // 退出写模式
}
}
}这个工具在我处理高性能缓存或配置读取时,能显著提升并发度,因为大部分操作都是读取。
6. Interlocked
对于简单的数值类型(如
int
long
Interlocked
lock
public class AtomicCounter
{
private int _count = 0;
public void Increment()
{
Interlocked.Increment(ref _count); // 原子增量
}
public int GetCount()
{
return Interlocked.CompareExchange(ref _count, _count, _count); // 原子读取,等同于直接返回_count,但确保了原子性
}
}当只需要对一个简单变量进行原子更新时,
Interlocked
说实话,这个问题问得非常到位,因为很多人在写多线程代码时,往往只想着“并行”,而忽略了“同步”的重要性,直到bug出现才追悔莫及。
我们需要线程同步的核心原因,在于多线程环境下对共享资源的并发访问可能导致一系列灾难性的后果,最常见也最头疼的就是“竞态条件”(Race Condition)。
竞态条件(Race Condition): 当多个线程尝试同时访问和修改同一个共享资源时,最终结果的正确性取决于线程执行的时序,这种时序是不可预测的。举个最经典的例子,如果两个线程都想对一个共享的计数器
_count
_count++
_count
_count
_count
_count
_count
数据不一致性: 竞态条件直接导致的就是数据不一致。除了上面计数器的例子,想象一下一个复杂对象,多个字段需要同时更新,但如果只更新了一部分字段,另一个线程就读取了这个“半成品”状态,那么得到的数据就是错误的,可能会引发后续的逻辑错误甚至程序崩溃。
死锁(Deadlock): 这是多线程编程中最令人头疼的问题之一。当两个或多个线程互相持有对方需要的资源,并都在等待对方释放资源时,就会发生死锁。它们会永远等待下去,导致程序卡住,无法继续执行。 例如:
活锁(Livelock): 活锁不如死锁常见,但同样危险。它指的是线程虽然没有被阻塞,但它们却在不断地改变状态,以响应其他线程的动作,结果导致没有任何实际进展。它们都在“谦让”,但最终谁也无法完成任务。比如两个人同时过独木桥,发现对方也过来了,于是都退回去,然后又同时尝试,又退回去,循环往复。
性能下降: 虽然线程同步是为了保证正确性,但过度或不恰当的同步也会带来性能开销。锁的获取和释放本身就需要时间,如果锁粒度过大,导致大量线程长时间等待,那么程序的并行性就会大大降低,甚至不如单线程执行效率高。
所以,线程同步是多线程编程中不可或缺的环节。它就像交通规则,虽然会限制车辆的速度和自由,但却是保证整个交通系统安全、高效运行的关键。
这没有一个放之四海而皆准的答案,更多的是一种权衡和艺术。在我看来,选择合适的同步机制,就像是选择合适的工具来解决一个问题,你需要考虑问题的性质、性能要求、代码复杂度和可维护性。
1. 优先考虑 lock
lock
lock
2. 考虑 Monitor
Monitor.Wait
Monitor.Pulse/PulseAll
lock
3. ReaderWriterLockSlim
ReaderWriterLockSlim
4. SemaphoreSlim
SemaphoreSlim
5. Interlocked
int
long
Interlocked
Interlocked
6. Mutex
Mutex
7. 现代并发集合 (Concurrent Collections
ConcurrentDictionary<TKey, TValue>
ConcurrentQueue<T>
ConcurrentBag<T>
Dictionary
Queue
8. async/await
Task Parallel Library (TPL)
async/await
Parallel.For
Parallel.ForEach
async/await
总结一下我的选择逻辑:
lock
ReaderWriterLockSlim
SemaphoreSlim
Interlocked
Concurrent Collections
Monitor
Mutex
选择的关键在于理解每种机制的优缺点和适用场景,避免过度同步(导致性能下降)和同步不足(导致数据错误)。
C#和.NET平台在并发编程方面一直在进化,除了那些底层的锁机制,我们现在有更多高级、更抽象的工具和模式,它们旨在简化并发代码的编写,提高可读性和可维护性,同时尽可能地减少手动管理锁的需要。
1. async/await
async/await
Task
Task
async/await
2. Task Parallel Library (TPL)
Parallel.For
Parallel.ForEach
// 示例:并行计算数组平方
int[] numbers = Enumerable.Range(0, 1000000).ToArray();
long[] squares = new long[numbers.Length];
Parallel.For(0, numbers.Length, i =>
{
squares[i] = (long)numbers[i] * numbers[i];
});Parallel.Invoke
Task
Task<TResult>
Task
Task
ContinueWith
Task
async/await
3. 并发集合 (Concurrent Collections): 正如前面提到的,这是我非常推崇的一类工具。它们是 .NET Framework 4.0 引入的,位于
System.Collections.Concurrent
ConcurrentDictionary<TKey, TValue>
ConcurrentQueue<T>
ConcurrentBag<T>
ConcurrentStack<T>
lock
ConcurrentDictionary
Dictionary
ConcurrentQueue
Queue
ConcurrentBag
4. Channel<T>
Queue
Monitor
public async Task RunChannelExample()
{
var channel = Channel.CreateUnbounded<int>(); // 创建一个无界通道
// 生产者
_ = Task.Run(async () =>
{
for (int i = 0; i < 10; i++)
{
await channel.Writer.WriteAsync(i);
Console.WriteLine($"Produced: {i}");
await Task.Delay(100);
}
channel.Writer.Complete(); // 生产者完成,关闭通道
});
// 消费者
await foreach (var item in channel.Reader以上就是C#的线程同步是什么?如何实现?的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号