并发编程 - 锁(NSLock)

引言

在多线程编程中,数据一致性是一个必须解决的问题。多个线程同时访问同一片共享数据时,极易发生竞争条件(race conditions),导致数据的不一致性,甚至程序崩溃。为了解决这些问题,我们需要引入一种机制,确保多个线程之间的操作可以安全地进行。

我们之前讨论了最基本的自旋锁——属性修饰符(Atomic),它能够保证单个数据的多线程操作安全,但无法解决复合数据一致性的问题。接着,我们介绍了Objective-C中使用最简单的锁机制——@synchronized。它的使用非常简洁实用,适用于大多数场景,但我们也提到过在锁中@synchronized的性能并不高,不太适合在高并发场景中使用,并且对于锁对象的选择也有一定的要求。

那么,是否存在一种既适合高并发场景又简单易用的锁呢?答案是肯定的!今天,我们就来探讨一下NSLock。

NSLock简介

NSLock是另外一种用于实现线程同步的锁机制,提供了轻量级的锁定方式,能够有效地防止多个线程同时访问共享资源,从而确保数据的一致性。

特点

  1. 轻量型:NSLock的实现相对简单,开销小,适合需要频繁锁定和解锁的场景。
  2. 可重入性:NSLock本身并不支持递归锁定,这一点需要注意,这意味着同一现场不能多次锁定同一个NSLock实例,这有助于避免复杂的死锁问题。
  3. 灵活性:NSLock提供了更为灵活的锁定方式,允许开发者在需要时手动控制锁定和解锁操作。
  4. 性能优势:在高并发场景中,NSLock相比@synchronized具有更好的性能表现。

与@synchronized对比

  1. 性能:@synchronized的性能开销相对较高,因为它需要进行额外的消息传递,而NSLock直接调用锁定方法,性能更优。
  2. 锁对象的选择:@synchronized需要一个对象作为锁,而NSLock是一个独立的实例,可以更灵活地管理锁定。
  3. 易用性:@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的特点和使用场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值