C# lock
在C#中,lock
关键字用于确保当一个线程位于给定实例的代码块中时,其他线程无法访问同一实例的该代码块。这是一种简单的同步机制,用来防止多个线程同时访问共享资源或执行需要独占访问的代码段(临界区),从而避免竞态条件和数据不一致问题。
使用方式
lock
语句的基本语法如下:
lock (expression)
{// 需要同步的代码块
}
这里的expression
必须是一个可以被引用的对象,通常是一个私有的、专门用于锁定目的的对象。lock
实际上是对Monitor.Enter
和Monitor.Exit
方法的封装,并且它保证了即使在发生异常的情况下也会正确释放锁。
工作原理
- 当一个线程到达
lock
语句时,它会尝试获取由expression
指定对象的锁。 - 如果锁是可用的(即没有其他线程持有该锁),则该线程获得锁并进入临界区执行代码。
- 如果锁已经被另一个线程持有,则当前线程将被阻塞,直到锁被释放。
- 一旦线程完成临界区内的操作,
lock
确保调用Monitor.Exit
来释放锁,这样等待中的其他线程就可以继续执行。
注意事项
-
唯一性:建议为每个需要保护的共享资源使用独立的锁对象。不要使用公共对象如
this
或类型本身(typeof(TypeName)
)作为锁对象,以避免不必要的锁竞争。 -
不可变性:作为锁的对象最好是不可变的(immutable),因为如果锁对象的状态可以改变,可能会导致不确定的行为。
-
引用类型:只能对引用类型的对象加锁。值类型不能用于
lock
,因为每次装箱都会创建一个新的对象,这将破坏锁定的目的。 -
性能考虑:虽然
lock
是实现简单同步的有效手段,但过度使用或不当使用可能导致性能瓶颈甚至死锁。尽量减少锁的作用范围,并考虑使用更高级的并发工具如ReaderWriterLockSlim
、ConcurrentDictionary
等。 -
避免死锁:设计多线程程序时要注意避免死锁,即两个或更多的线程互相等待对方释放锁的情况。一种预防措施是保持一致的锁获取顺序。
示例代码
下面是一个简单的例子,演示如何使用lock
来保护共享资源:
private readonly object lockObject = new object();
private int counter = 0;public void IncrementCounter()
{lock (lockObject){counter++;}
}
在这个例子中,lockObject
是用来保护counter
变量的锁。通过这种方式,我们可以确保在同一时间只有一个线程能够修改counter
的值,从而避免竞态条件。
为什么不能lock值类型
在C#中,lock
语句要求其参数必须是一个引用类型的对象,而不能是值类型。这是因为lock
机制依赖于对象的引用标识来实现同步控制,具体来说,lock
实际上是对指定对象进行加锁操作,确保同一时间只有一个线程能够执行被锁定保护的代码块。
当你尝试对一个值类型使用lock
时,会发生以下情况:
-
装箱(Boxing):由于
lock
只能接受引用类型作为参数,因此如果传递了一个值类型,编译器会自动对该值类型进行装箱操作,将其转换为一个引用类型(即Object类型的一个实例)。这意味着每次执行lock
时都会创建一个新的对象。 -
失去锁定的意义:由于每次装箱都会创建一个新的对象,即使你多次对同一个值类型变量使用
lock
,它们实际上是在不同的对象上加锁。因此,这不会产生预期的同步效果,因为不同线程可能会同时获取到“锁”,导致竞态条件的发生。
例如,下面的代码试图对一个整型变量进行锁定,但实际上并不会按预期工作:
int number = 0;
lock (number) // 编译错误:无法对值类型使用 lock 语句
{// Do something...
}
正确的做法是使用一个专门的引用类型对象作为锁对象。通常,我们会定义一个私有的、只读的对象用于锁定目的,以避免意外的锁竞争和确保锁定的有效性。例如:
private readonly object lockObject = new object();
int number = 0;lock (lockObject)
{number++;
}
这样做的好处是可以确保所有希望同步访问共享资源的线程都在同一个对象上等待锁,从而达到预期的同步效果。总之,为了避免上述问题并正确地实现同步逻辑,应始终使用引用类型的对象作为lock
的目标。
参考链接