目录
[八、 常见误区](#八、 常见误区)
一、 什么是临界区?
在多线程编程中,临界区是指一段需要互斥访问的代码块,通常涉及对共享资源的操作。为了避免多个线程同时操作共享资源而导致数据竞争或状态不一致,我们需要对临界区代码进行保护。
下面通过一个简单的例子来说明什么是临界区代码,以及如何使用 lock 来保护它。
示例:两个线程操作共享变量
假设我们有一个共享变量 _counter,两个线程分别对其进行递增操作。如果不对临界区代码进行保护,可能会导致数据竞争和错误结果。

二、 lock关键字的用途
lock关键字在C#中用于确保当一个线程访问某个资源时,其他线程不能同时访问该资源,从而避免了数据竞争和不一致性的问题。它通过提供一种简单的方式实现线程同步,保证多线程环境下共享资源的安全访问。
三、 lock的基本用法
cs
private static readonly object _lock = new object(); // 锁对象
lock (object)
{
// 需要同步的代码块
}
- object 是一个引用类型的对象,通常称为锁对象。
- 当线程进入lock语句时,会尝试获取锁对象的互斥锁。如果成功获取锁,则执行代码块;否则,线程会被阻塞,直到锁被释放。
四、 lock关键字的工作原理
lock实际上是Monitor.Enter和Monitor.Exit的语法糖
当线程进入lock语句时:
- 调用Monitor.Enter(object)获取锁。
- 如果锁已被其他线程占用,则当前线程会被挂起,直到锁被释放。
当线程退出lock语句时:
- 调用Monitor.Exit(object)释放锁。
五、 示例1- 保护共享变量
假设有一个共享变量_counter,多个线程可能会同时修改它的值。为了确保线程安全,可以使用lock来保护对_counter的访问。
cs
using System;
using System.Threading;
class Program
{
private static int _counter = 0;
private static readonly object _lock = new object();
static void Main()
{
Thread t1 = new Thread(IncrementCounter);
Thread t2 = new Thread(IncrementCounter);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"Final Counter Value: {_counter}");
}
static void IncrementCounter()
{
for (int i = 0; i < 100000; i++)
{
lock (_lock)
{
_counter++;
}
}
}
}
解释:
- _lock是一个静态对象,作为锁对象。
- 每次访问_counter时,都会通过lock确保只有一个线程能够修改它。
- 最终输出的结果是200000,因为所有线程的操作都被正确同步了。
六、 示例2-与Monitor类配合实现线程间信号传递
假设我们有两个线程:
- 线程A:等待某个条件满足后再继续执行。
- 线程B:负责触发这个条件,并通知线程A继续执行。
cs
using System;
using System.Threading;
class Program
{
private static readonly object _lock = new object();
private static bool _isReady = false; // 共享的状态变量
static void Main(string[] args)
{
Thread threadA = new Thread(DoWorkA);
Thread threadB = new Thread(DoWorkB);
threadA.Start();
threadB.Start();
threadA.Join();
threadB.Join();
}
static void DoWorkA()
{
lock (_lock)
{
Console.WriteLine("Thread A: Waiting for signal...");
// 等待条件满足
while (!_isReady)
{
Monitor.Wait(_lock); // 释放锁并等待
}
Console.WriteLine("Thread A: Received signal. Continuing work.");
}
}
static void DoWorkB()
{
Thread.Sleep(2000); // 模拟一些工作
lock (_lock)
{
Console.WriteLine("Thread B: Preparing to signal...");
// 设置条件为 true
_isReady = true;
// 通知等待的线程
Monitor.Pulse(_lock);
Console.WriteLine("Thread B: Signal sent.");
}
}
}
****注意:****即使线程A和线程B中的临界区代码不同,只要它们使用的是同一个锁对象 _lock,系统就会保证这两个线程不会同时执行这些临界区代码。
七、 注意事项
(1) 锁对象的选择
- 锁对象必须是引用类型(如object、string等),不能是值类型(如int、struct等)。
- 推荐使用专用的私有对象作为锁对象,而不是公共对象或字符串常量。例如:
cs
private static readonly object _lock = new object();
- 不要使用this作为锁对象,因为外部代码可能也会锁定该对象,导致死锁风险。
- 锁对象尽量设置为readonly,如果 _lock 是一个普通的对象(非 readonly),它可能会被意外修改或重新赋值,在这种情况下,其他线程可能尝试锁定一个新的 _lock 对象,而不是原来的对象,从而破坏了同步逻辑。
(2) 避免死锁
- 死锁是指两个或多个线程互相等待对方释放锁,导致程序无法继续运行。
- 避免死锁的方法包括:
- 确保锁的获取顺序一致。
- 尽量减少锁的范围,只锁定必要的代码块。
(3) 性能影响
- 使用lock会导致线程阻塞,因此可能会影响性能。
- 对于高并发场景,可以考虑使用其他同步机制(如SemaphoreSlim、ReaderWriterLockSlim等)。
八、 常见误区
(1) 锁的范围过大
- 如果锁的范围过大,会导致线程阻塞时间过长,降低程序的并发性能。
- 应尽量缩小锁的范围,只锁定需要同步的代码部分。
(2) 忘记释放锁
- 在正常情况下,lock语句会自动释放锁。但如果在 lock 代码块中抛出异常且未正确处理,可能会导致锁无法释放。
- 确保lock代码块中的逻辑是安全的,避免抛出未捕获的异常。
九、 替代方案
对于某些特定的应用场景,可以考虑使用以下替代方案:
- Monitor 类:直接使用Monitor可以提供更细粒度的控制。
- Mutex:适用于跨进程的同步。
- SemaphoreSlim:适用于限制并发线程数。
- ReaderWriterLockSlim:适用于读多写少的场景。