iOS 知识点原理iOS

iOS中常见的几种锁

2019-07-30  本文已影响0人  6ffd6634d577

线程安全是什么?

当一个线程访问数据的时候,其他的线程不能对其进行访问,直到该线程访问完毕。简单来讲就是在同一时刻,对同一个数据操作的线程只有一个。只有确保了这样,才能使数据不会被其他线程影响。而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。

比如写文件和读文件,当一个线程在写文件的时候,理论上来说,如果这个时候另一个线程来直接读取的话,那么得到的结果可能是你无法预料的。

什么情况下会造成死锁?

死锁就是队列引起的循环等待
1、一个比较常见的死锁例子:主队列同步

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"deallock");
    });
}

同步对于任务是立刻执行的,那么当把任务放进主队列时,它就会立马执行,只有执行完这个任务,viewDidLoad才会继续向下执行。
而viewDidLoad和任务都是在主队列上的,由于队列的先进先出原则,任务又需等待viewDidLoad执行完毕后才能继续执行,viewDidLoad和这个任务就形成了相互循环等待,就造成了死锁。
想避免这种死锁,可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。

2、串行队列(当然也包括主队列)中向这个队列同步添加任务都会造成死锁

异步执行一定会开启子线程吗???
不一定,在主线程中异步+主队列就不会

    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"异步执行一定会创建子线程吗???");
        NSLog(@"%@",[NSThread currentThread]);
    });
//打印结果:
 <NSThread: 0x600003154d80>{number = 1, name = main}

怎么来保证线程安全?

通常我们使用锁的机制来保证线程安全,即确保同一时刻只有同一个线程来对同一个数据源进行访问。

YY大神 的 不再安全的 OSSpinLock 这边博客中列出了各种锁以及性能比较:

image.png

这里性能比较的只是加锁立马解锁的时间消耗,并没有计算竞争时候的时间消耗。iOS开发中常用的锁有如下几种:

  1. @synchronized
  2. NSLock 对象锁
  3. NSRecursiveLock 递归锁
  4. NSConditionLock 条件锁
  5. pthread_mutex 互斥锁(C语言)
  6. dispatch_semaphore 信号量实现加锁(GCD)
  7. OSSpinLock 自旋锁(暂不建议使用,原因参见这里

1.@synchronized

@synchronized 关键字加锁 互斥锁,性能较差不推荐使用

@synchronized(这里添加一个OC对象,一般使用self) { 
        //这里写要加锁的代码
 }  

注意点   
1.加锁的代码尽量少    
2.添加的OC对象必须在多个线程中都是同一对象 
3.优点是不需要显式的创建锁对象,便可以实现锁的机制。 
4. @synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。
所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。

下面通过 卖票的例子 展示使用:
-(void)synchronizedTest{
    //设置票的数量为5
     _tickets = 5;
    //线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self saleTickets];
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self saleTickets];
    });
}

- (void)saleTickets {
    while (1) {
        @synchronized(self) {
            [NSThread sleepForTimeInterval:1];
            if (_tickets > 0) {
                _tickets--;
                NSLog(@"剩余票数= %d, Thread:%@",_tickets,[NSThread currentThread]);
            }else {
                NSLog(@"票卖完了 Thread:%@",[NSThread currentThread]);
                break;
            }
        }
    }
}

下面是加锁的和没加锁的对比:


image.png
image.png

NSLock

先看看iOS中NSLock类的.h文件,从代码中可以看出,该类分成了几个子类:NSLockNSConditionLockNSRecursiveLockNSCondition,然后有一个 NSLocking 协议

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

虽然 NSLock、NSConditionLock、NSRecursiveLock、NSCondition 都遵循的了 NSLocking 协议,但是它们并不相同。

2.1 NSLock
NSLock 实现了最基本的互斥锁,遵循了 NSLocking 协议,通过 lock 和 unlock 来进行锁定和解锁。

源码内容:

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

NSLock类还增加了tryLocklockBeforeDate:方法。
tryLock试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程,相反,它只是返回NO。
这里顺便提一下 trylock 和 lock 使用场景:
当前线程锁失败,也可以继续其它任务,用 trylock 合适;当前线程只有锁成功后,才会做一些有意义的工作,那就 lock,没必要轮询 trylock。以下的锁都是这样。

lockBeforeDate:方法试图获取一个锁,但是如果锁没有在规定的时间内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回NO)。

注意:NSLock 互斥锁 不能多次调用 lock方法,会造成死锁

-(void)NSLockTest{
    //设置票的数量为5
    _tickets = 5;
    //创建锁
    _mutexLock = [[NSLock alloc] init];
    //线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self saleTickets];
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self saleTickets];
    });
}

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        //加锁
        [_mutexLock lock];
        if (_tickets > 0) {
            _tickets--;
            NSLog(@"剩余票数= %d, Thread:%@",_tickets,[NSThread currentThread]);
        } else {
            NSLog(@"票卖完了 Thread:%@",[NSThread currentThread]); break;
        }
        //解锁
        [_mutexLock unlock];
    }
}
image.png

2.2 NSRecursiveLock
NSRecursiveLock 是递归锁,顾名思义,可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。NSRecursiveLock 会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。

源码内容:

@interface NSRecursiveLock : NSObject <NSLocking> {
@private
  void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

示例代码:

- (void)nsrecursivelock{
    NSRecursiveLock * cjlock = [[NSRecursiveLock alloc] init];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^RecursiveBlock)(int);
        RecursiveBlock = ^(int value) {
            [cjlock lock];
            NSLog(@"%d加锁成功",value);
            if (value > 0) {
                NSLog(@"value:%d", value);
                RecursiveBlock(value - 1);
            }
            [cjlock unlock];
            NSLog(@"%d解锁成功",value);
        };
        RecursiveBlock(3);
    });
}
image.png

由以上内容总结:

如果用 NSLock 的话,cjlock 先锁上了,但未执行解锁的时候,就会进入递归的下一层,而再次请求上锁,阻塞了该线程,线程被阻塞了,自然后面的解锁代码不会执行,而形成了死锁。NSRecursiveLock 递归锁就是为了解决这个问题。

2.3 NSConditionLock
NSConditionLock 对象所定义的互斥锁可以在使得在某个条件下进行锁定和解锁,它和 NSLock 类似,都遵循 NSLocking 协议,方法都类似,只是多了一个 condition 属性,以及每个操作都多了一个关于 condition 属性的方法,例如tryLock、tryLockWhenCondition:,所以 NSConditionLock 可以称为条件锁。

源码内容:

@interface NSConditionLock : NSObject <NSLocking> {
@private
  void *_priv;
}

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

示例代码:

- (void)nsconditionlock {
    NSConditionLock * cjlock = [[NSConditionLock alloc] initWithCondition:0];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [cjlock lock];
        NSLog(@"线程1加锁成功");
        sleep(1);
        [cjlock unlock];
        NSLog(@"线程1解锁成功");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [cjlock lockWhenCondition:1];
        NSLog(@"线程2加锁成功");
        [cjlock unlock];
        NSLog(@"线程2解锁成功");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(2);
        if ([cjlock tryLockWhenCondition:0]) {
            NSLog(@"线程3加锁成功");
            sleep(2);
            [cjlock unlockWithCondition:2];
            NSLog(@"线程3解锁成功");
        } else {
            NSLog(@"线程3尝试加锁失败");
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if ([cjlock lockWhenCondition:2 beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]) {
            NSLog(@"线程4加锁成功");
            [cjlock unlockWithCondition:1];
            NSLog(@"线程4解锁成功");
        } else {
            NSLog(@"线程4尝试加锁失败");
        }
    });
}
image.png

由以上内容总结:

2.4、NSCondition
NSCondition 是一种特殊类型的锁,通过它可以实现不同线程的调度。一个线程被某一个条件所阻塞,直到另一个线程满足该条件从而发送信号给该线程使得该线程可以正确的执行。比如说,你可以开启一个线程下载图片,一个线程处理图片。这样的话,需要处理图片的线程由于没有图片会阻塞,当下载线程下载完成之后,则满足了需要处理图片的线程的需求,这样可以给定一个信号,让处理图片的线程恢复运行。

源码内容:

@interface NSCondition : NSObject <NSLocking> {
@private
  void *_priv;
}

- (void)wait; //挂起线程
- (BOOL)waitUntilDate:(NSDate *)limit; //什么时候挂起线程
- (void)signal; // 唤醒一条挂起线程
- (void)broadcast; //唤醒所有挂起线程

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

示例代码:

- (void)nscondition {
  NSCondition * cjcondition = [NSCondition new];
  
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [cjcondition lock];
    NSLog(@"线程1线程加锁");
    [cjcondition wait];
    NSLog(@"线程1线程唤醒");
    [cjcondition unlock];
    NSLog(@"线程1线程解锁");
  });
  
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [cjcondition lock];
    NSLog(@"线程2线程加锁");
    if ([cjcondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]]) {
      NSLog(@"线程2线程唤醒");
      [cjcondition unlock];
      NSLog(@"线程2线程解锁");
    }
  });
  
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
    sleep(2);
    [cjcondition signal];
  });
}
image.png
//如果 [cjcondition signal]; 改成 [cjcondition broadcast];
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
    sleep(2);
    [cjcondition broadcast];
  });
image.png

由以上内容总结:

3.dispatch_semaphore

dispatch_semaphore 使用信号量机制实现锁,等待信号和发送信号。

常用相关API:

dispatch_semaphore_create(long value);
dispatch_semaphore_wait(dispatch_semaphore_t _Nonnull dsema, dispatch_time_t timeout);
dispatch_semaphore_signal(dispatch_semaphore_t _Nonnull dsema);

实例代码:

- (void)dispatch_semaphore {
  dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
  dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 6 * NSEC_PER_SEC);

  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_semaphore_wait(semaphore, overTime);
    NSLog(@"线程1开始");
    sleep(5);
    NSLog(@"线程1结束");
    dispatch_semaphore_signal(semaphore);
  });
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(1);
    dispatch_semaphore_wait(semaphore, overTime);
    NSLog(@"线程2开始");
    dispatch_semaphore_signal(semaphore);
  });
}
image.png
//如果 overTime 改成 3 秒
image.png

由以上内容总结:

4.pthread_mutex 与 pthread_mutex(recursive)

pthread表示 POSIX thread,定义了一组跨平台的线程相关的 API,POSIX 互斥锁是一种超级易用的互斥锁,使用的时候:

常用相关API:

pthread_mutex_init(pthread_mutex_t *restrict _Nonnull, const pthread_mutexattr_t *restrict _Nullable);
pthread_mutex_lock(pthread_mutex_t * _Nonnull);
pthread_mutex_trylock(pthread_mutex_t * _Nonnull);
pthread_mutex_unlock(pthread_mutex_t * _Nonnull);
pthread_mutex_destroy(pthread_mutex_t * _Nonnull);

示例代码:

//记得导入头文件
 #include <pthread.h>

- (void)pthread_mutex {
  __block pthread_mutex_t cjlock;
  pthread_mutex_init(&cjlock, NULL);
  
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    pthread_mutex_lock(&cjlock);
    NSLog(@"线程1开始");
    sleep(3);
    NSLog(@"线程1结束");
    pthread_mutex_unlock(&cjlock);
    
  });
  
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(1);
    pthread_mutex_lock(&cjlock);
    NSLog(@"线程2");
    pthread_mutex_unlock(&cjlock);
  });
}
image.png
//pthread_mutex(recursive)

- (void)pthread_mutex_recursive {
  __block pthread_mutex_t cjlock;
  
  pthread_mutexattr_t attr;
  pthread_mutexattr_init(&attr);
  pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
  pthread_mutex_init(&cjlock, &attr);
  pthread_mutexattr_destroy(&attr);
  
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
    static void (^RecursiveBlock)(int);
    
    RecursiveBlock = ^(int value) {
      pthread_mutex_lock(&cjlock);
      NSLog(@"%d加锁成功",value);
      if (value > 0) {
        NSLog(@"value = %d", value);
        sleep(1);
        RecursiveBlock(value - 1);
      }
      NSLog(@"%d解锁成功",value);
      pthread_mutex_unlock(&cjlock);
    };
    RecursiveBlock(3);
  });
}
image.png

由以上内容总结:

5. OSSpinLock

OSSpinLock 是一种自旋锁,和互斥锁类似,都是为了保证线程安全的锁。但二者的区别是不一样的,对于互斥锁,当一个线程获得这个锁之后,其他想要获得此锁的线程将会被阻塞,直到该锁被释放。但自选锁不一样,当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放。所以,此锁比较适用于锁的持有者保存时间较短的情况下。

自旋锁加锁的时候,等待锁的线程处于忙等状态,并且占用着CPU的资源。
互斥锁加锁的时候,等待锁的线程处于休眠状态,不会占用CPU的资源。

只有加锁,解锁,尝试加锁三个方法。

常用相关API:

typedef int32_t OSSpinLock;

// 加锁
void  OSSpinLockLock( volatile OSSpinLock *__lock );
// 尝试加锁
bool  OSSpinLockTry( volatile OSSpinLock *__lock );
// 解锁
void  OSSpinLockUnlock( volatile OSSpinLock *__lock );

示例代码:

//使用的时候Xcode会提示已过期,使用os_unfair_lock()替代
'OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from <os/lock.h> instead

#import <libkern/OSAtomic.h>

- (void)osspinlock {
    __block OSSpinLock theLock = OS_SPINLOCK_INIT;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(&theLock);
        NSLog(@"线程1开始");
        sleep(3);
        NSLog(@"线程1结束");
        OSSpinLockUnlock(&theLock);
        
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(&theLock);
        sleep(1);
        NSLog(@"线程2");
        OSSpinLockUnlock(&theLock);
        
    });
}
image.png

YY大神 @ibireme 的文章也有说这个自旋锁存在优先级反转问题,具体文章可以戳 不再安全的 OSSpinLock,而 OSSpinLock 在iOS 10.0中被 <os/lock.h> 中的 os_unfair_lock 取代。

6.os_unfair_lock

自旋锁已经不再安全,然后苹果又整出来个 os_unfair_lock,这个锁解决了优先级反转问题。

注意:os_unfair_lock 是互斥锁

常用相关API:

// 初始化
os_unfair_lock_t unfairLock = &(OS_UNFAIR_LOCK_INIT);
// 加锁
os_unfair_lock_lock(unfairLock);
// 尝试加锁
BOOL b = os_unfair_lock_trylock(unfairLock);
// 解锁
os_unfair_lock_unlock(unfairLock);
os_unfair_lock 用法和 OSSpinLock 基本一直,就不一一列出了。

总结

应当针对不同的操作使用不同的锁,而不能一概而论哪种锁的加锁解锁速度快。

其实每一种锁基本上都是加锁、等待、解锁的步骤,理解了这三个步骤就可以帮你快速的学会各种锁的用法。

@synchronized 的效率最低,不过它的确用起来最方便,所以如果没什么性能瓶颈的话,可以选择使用 @synchronized。

性能要求较高时候,可以使用pthread_mutex 或者 dispath_semaphore,由于 OSSpinLock 不能很好的保证线程安全,而在只有在 iOS10 中才有 os_unfair_lock ,所以,前两个是比较好的选择。既可以保证速度,又可以保证线程安全。

对于 NSLock 及其子类,时间消耗来说 NSLock < NSCondition < NSRecursiveLock < NSConditionLock

上一篇下一篇

猜你喜欢

热点阅读