引言
在多线程编程中,数据一致性是一个必须解决的问题。多个线程同时访问同一片共享数据时,极易发生竞争条件(race conditions),导致数据的不一致性,甚至程序崩溃。为了解决这些问题,我们需要引入一种机制,确保多个线程之间的操作可以安全地进行。
我们之前讨论了最基本的自旋锁——属性修饰符(Atomic),它能够保证单个数据的多线程操作安全,但无法解决复合数据一致性的问题。接着,我们介绍了Objective-C中使用最简单的锁机制——@synchronized。它的使用非常简洁实用,适用于大多数场景,但我们也提到过在锁中@synchronized的性能并不高,不太适合在高并发场景中使用,并且对于锁对象的选择也有一定的要求。
那么,是否存在一种既适合高并发场景又简单易用的锁呢?答案是肯定的!今天,我们就来探讨一下NSLock。
NSLock简介
NSLock是另外一种用于实现线程同步的锁机制,提供了轻量级的锁定方式,能够有效地防止多个线程同时访问共享资源,从而确保数据的一致性。
特点
- 轻量型:NSLock的实现相对简单,开销小,适合需要频繁锁定和解锁的场景。
- 可重入性:NSLock本身并不支持递归锁定,这一点需要注意,这意味着同一现场不能多次锁定同一个NSLock实例,这有助于避免复杂的死锁问题。
- 灵活性:NSLock提供了更为灵活的锁定方式,允许开发者在需要时手动控制锁定和解锁操作。
- 性能优势:在高并发场景中,NSLock相比@synchronized具有更好的性能表现。
与@synchronized对比
- 性能:@synchronized的性能开销相对较高,因为它需要进行额外的消息传递,而NSLock直接调用锁定方法,性能更优。
- 锁对象的选择:@synchronized需要一个对象作为锁,而NSLock是一个独立的实例,可以更灵活地管理锁定。
- 易用性:@synchronized使用起来非常简单,代码更简洁;而NSlock虽然需要手动管理锁定和解锁,但提供了更大的灵活性。
NSLock用法
NSLock和其对象一样进行实例化,另外NSLock提供两个方法进行加锁和解锁。
- 锁定:使用lock方法来获取锁。
- 解锁:使用unlock方法来释放锁。
初始化
NSLock *lock = [[NSLock alloc] init];
加锁,解锁
[lock lock]; // 锁定
// 需要线程安全的代码
[lock unlock]; // 解锁
使用起来非常简单,接下来我们仍然以多个线程同时操作共享数组为例来使用NSLock,具体代码如下:
定义一个NSLock和一个NSMutableArray。
@interface ViewController ()
/// 锁
@property(nonatomic,strong)NSLock * lock;
/// 数组
@property(nonatomic,strong)NSMutableArray * arrayM;
@end
开启多线向数组中添加元素。
//MARK: NSLock
- (void)lockTest {
self.arrayM = [NSMutableArray array];
self.lock = [[NSLock alloc] init];
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.arrayM addObject:[NSString stringWithFormat:@"河北省唐山市路北区%d", i]];
});
}
NSLog(@"arrayM:%@",self.arrayM);
}
如果不加任何限制,那么代码执行起来大概率会引发崩溃。
接下来我们使用NSLock把添加元素的事件进行加锁,执行完成后把锁释放:
//MARK: NSLock
- (void)lockTest {
self.arrayM = [NSMutableArray array];
self.lock = [[NSLock alloc] init];
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.lock lock];
[self.arrayM addObject:[NSString stringWithFormat:@"河北省唐山市路北区%d", i]];
[self.lock unlock];
});
}
NSLog(@"arrayM:%@",self.arrayM);
}
该方法就可以顺利执行了,需要注意:
- 锁定和解锁必须在同一线程。
- 确保每次锁定后都进行解锁,以避免死锁的发生。
- 锁定和解锁应尽量放在最小范围内,以提高并发性能。
NSLock的局限
NSLock作为一个轻量级的锁使用起来非常简单,在实际的项目开发中很受开发者们的青睐,但是它有一些不能胜任的场面,下面我们来看下一个例子。
我们先来模拟一个最简单的场景,在一段加锁的代码中调用另外一个方法,而另一个方法可能在其它地方也会调用,所以也加了锁,具体代码如下:
定义锁和name属性
@interface ViewController ()
/// 锁
@property(nonatomic,strong)NSLock * lock;
/// firstName
@property(nonatomic,strong)NSString * firstName;
/// lastName
@property(nonatomic,strong)NSString * lastName;
/// name
@property(nonatomic,strong)NSString * name;
@end
修改firstName的方法
//MARK: 修改firstName
- (void)changeFirstName {
[self.lock lock];
self.firstName = @"张";
NSLog(@"firstName:%@",self.firstName);
[self changeName];
[self.lock unlock];
}
修改lastName的方法
//MARK: 修改lastName
- (void)changeLastName {
[self.lock lock];
self.lastName = @"三";
NSLog(@"lastName:%@",self.lastName);
[self changeName];
[self.lock unlock];
}
修改name的方法
//MARK: 修改name
- (void)changeName {
[self.lock lock];
self.name = [NSString stringWithFormat:@"%@%@",self.firstName,self.lastName];
NSLog(@"name:%@",self.name);
[self.lock unlock];
}
每次修改firstName或者是lastName时,同时还会修改name。并且每个操作都是线程安全的,我们使用NSLock进行了加锁。
接下来我们使用另外一个方法来修改firstName
//MARK: NSLock
- (void)lockTest {
if (self.lock == nil) {
self.lock = [[NSLock alloc] init];
}
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self changeFirstName];
});
}
}
执行lockTest方法之后我们会发现控制台只打印了一次:
firstName:张
就再也没有反应了,因为我们在未调用unlock方法之前,又调用了一次lock导致锁死。也就是说NSLock锁不可以嵌套,重入。
下面我们来看另外一个案例。
//MARK: NSLock
- (void)lockTest {
if (self.lock == nil) {
self.lock = [[NSLock alloc] init];
}
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value) {
if (value > 0) {
NSLog(@"value:%d",value);
testMethod(value - 1);
}
};
testMethod(10);
});
}
}
在for循环中,获取了一个全局并发队列,然后开始递归执行代码块,如果没有for循环,代码可以顺利执行,并且打印结果如下:
value:10
value:9
value:8
value:7
value:6
value:5
value:4
value:3
value:2
value:1
加了for循环之后,按照预期,应该是上面的内容循环打印十遍,接下来我们执行代码看一下结果:
value:10
value:10
value:9
value:10
value:8
value:9
value:9
value:7
value:8
value:10
从结果可以看出,和我们预期的并不一样,由于多线程的问题,导致打印内容十分混乱。
那么我们需要进行加锁来解决这一问题。锁应该加在哪里呢?
首先我们将锁的范围放大一点,加在调用代码块的地方:
//MARK: NSLock
- (void)lockTest {
if (self.lock == nil) {
self.lock = [[NSLock alloc] init];
}
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value) {
if (value > 0) {
NSLog(@"value:%d",value);
testMethod(value - 1);
}
};
[self.lock lock];
testMethod(10);
[self.lock unlock];
});
}
}
会发现结果和我们期待的一样的:
value:10
value:9
value:8
value:7
value:6
value:5
value:4
value:3
value:2
value:1
value:10
value:9
value:8
...
但是如果锁被加到业务代码中呢?比如加在了函数或者是代码块的内部:
//MARK: NSLock
- (void)lockTest {
if (self.lock == nil) {
self.lock = [[NSLock alloc] init];
}
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value) {
if (value > 0) {
[self.lock lock];
NSLog(@"value:%d",value);
testMethod(value - 1);
[self.lock unlock];
}
};
testMethod(10);
});
}
}
允许代码发现,控制台只打印了一行。
value:10
这个写法会存在一个不断加锁而不解锁的情况,从而形成了递归加锁,但是NSLock是非递归锁。所以就产生了死锁。
由此我们可以得出NSLock必须在锁定的线程中进行解锁,不能嵌套,也非递归锁。
结语
在本篇博客中,我们探讨了NSLock作为一种简单而有效的线程同步机制,强调了它在高并发环境下的性能优势和使用的灵活性。通过对NSLock的特点、用法及注意事项的详细介绍,我们认识到它适合用于保护对共享资源的访问,确保数据一致性。
然而,NSLock也有其局限性,尤其是它的非递归特性使得在某些复杂场景中可能无法满足需求。因此对于需要支持递归的场景,我们需要更换另外一个锁——NSRecursiveLock。
在接下来的博客,我们会开始深入探讨NSRecursiveLock的特点和使用场景。