iOS 中锁的应用

2020-03-30  本文已影响0人  Queen_BJ
锁的应用是为了保证线程安全

多个线程访问同一块资源的时候,很容易引发数据混乱问题

基本概念
两种锁的应用

互斥锁用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑
1 临界区有IO操作
2 临界区代码复杂或者循环量大
3 临界区竞争非常激烈
4 单核处理器
至于自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下,自旋锁一般用于多核的服务器。

一、互斥锁

1、 @synchronized :
@synchronized (self) { //􏴉􏰼􏴧􏳋􏰑􏴩􏴪􏴉􏰼􏴧􏳋􏰑􏴩􏴪 需要锁定的代码 }
@synchronized 的作用是创建一个互斥锁,防止self对象在同一时间内被其它线程访问,起到线程的保护作用。
()内可以是任何的Objective-C对象

- (void)viewDidLoad {
    [super viewDidLoad];

    [self synchronized];
}

- (void)synchronized {
    NSObject * cjobj = [NSObject new];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @synchronized(cjobj){
            NSLog(@"线程1开始");
            sleep(3);
            NSLog(@"线程1结束");
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        @synchronized(cjobj){
            NSLog(@"线程2");
        }
    });
}

控制台输出:
2017-10-18 11:35:13.459194+0800 Thread-Lock[24855:431100] 线程1开始
2017-10-18 11:35:16.460210+0800 Thread-Lock[24855:431100] 线程1结束
2017-10-18 11:35:16.460434+0800 Thread-Lock[24855:431101] 线程2

注意:@synchronized(cjobj) 指令使用的 cjobj 为该锁的唯一标识,只有当标识相同时,才为满足互斥,
如果线程 2 中的 @synchronized(cjobj) 改为 @synchronized(self) ,那么线程 2 就不会被阻塞
优点:不需要在代码中显式的创建锁对象,便可以实现锁的机制
缺点:@synchronized 块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。

2、NSLock

NSLock * cjlock = [NSLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [cjlock lock];
    NSLog(@"线程1加锁成功");
    sleep(2);
    [cjlock unlock];
    NSLog(@"线程1解锁成功");
  });
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(1);
    [cjlock lock];
    NSLog(@"线程2加锁成功");
    [cjlock unlock];
    NSLog(@"线程2解锁成功");
  });

2020-03-28 15:56:57.580346+0800 iOSLockDemo[71692:3779253] 线程1加锁成功
2020-03-28 15:56:59.584020+0800 iOSLockDemo[71692:3779253] 线程1解锁成功
2020-03-28 15:56:59.584024+0800 iOSLockDemo[71692:3779258] 线程2加锁成功
2020-03-28 15:56:59.584163+0800 iOSLockDemo[71692:3779258] 线程2解锁成功

tryLock 返回 YES NO

   NSLock * cjlock = [NSLock new];
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [cjlock lock];
        NSLog(@"线程1加锁成功");
        sleep(2);
        [cjlock unlock];
        NSLog(@"线程1解锁成功");
    });
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if ([cjlock tryLock]) {
            NSLog(@"线程3加锁成功");
            [cjlock unlock];
            NSLog(@"线程3解锁成功");
        }else {
            NSLog(@"线程3加锁失败");
        }
    });
2020-03-28 16:08:23.535844+0800 iOSLockDemo[71997:3790084] 线程1加锁成功
2020-03-28 16:08:23.535844+0800 iOSLockDemo[71997:3790083] 线程3加锁失败
2020-03-28 16:08:25.539981+0800 iOSLockDemo[71997:3790084] 线程1解锁成功

lockBeforeDate

NSLock * cjlock = [NSLock new];
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [cjlock lock];
    NSLog(@"线程1加锁成功");
    sleep(2);
    [cjlock unlock];
    NSLog(@"线程1解锁成功");
  });
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    if ([cjlock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]) {
      NSLog(@"线程5加锁成功");
      [cjlock unlock];
      NSLog(@"线程5解锁成功");
    }else {
      NSLog(@"线程5加锁失败");
    }
  });

2020-03-28 16:06:39.145099+0800 iOSLockDemo[71974:3788856] 线程1加锁成功
2020-03-28 16:06:41.149201+0800 iOSLockDemo[71974:3788856] 线程1解锁成功
2020-03-28 16:06:41.149202+0800 iOSLockDemo[71974:3788857] 线程5加锁成功
2020-03-28 16:06:41.149957+0800 iOSLockDemo[71974:3788857] 线程5解锁成功

总结

3、thread_mutex 与 pthread_mutex(recursive):互斥锁(C语言)
pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,POSIX 互斥锁是一种超级易用的互斥锁,
使用的时候:
只需要使用 pthread_mutex_init 初始化一个 pthread_mutex_t,
pthread_mutex_lock 或者 pthread_mutex_trylock 来锁定 ,
pthread_mutex_unlock 来解锁,
pthread_mutex_destroy 来销毁锁。

导出#import <pthread.h>
#pragma mark -- pthread_mutex_t
-(void)pthread_mutex_twithMethod
{
    __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);
    });
}
2020-03-28 17:07:14.440619+0800 iOSLockDemo[72429:3813295] 线程1开始
2020-03-28 17:07:17.443088+0800 iOSLockDemo[72429:3813295] 线程1结束
2020-03-28 17:07:17.443386+0800 iOSLockDemo[72429:3813294] 线程2

二、自旋锁 OSSpinLock

自选锁,当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放。所以,此锁比较适用于锁的持有者保存时间较短的情况下。

SSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
...
OSSpinLockUnlock(&lock);

上面是OSSpinLock使用方式,编译会报警告,已经废弃了
os_unfair_lock:(互斥锁)
os_unfair_lock 是苹果官方推荐的替换OSSpinLock的方案,但是它在iOS10.0以上的系统才可以调用。os_unfair_lock是一种互斥锁,它不会向自旋锁那样忙等,而是等待线程会休眠。

//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);

#import "os_unfair_lockDemo.h"
#import <os/lock.h>
@interface os_unfair_lockDemo()
@property (assign, nonatomic) os_unfair_lock ticketLock;
@end

@implementation os_unfair_lockDemo
- (instancetype)init
{
self = [super init];
if (self) {
self.ticketLock = OS_UNFAIR_LOCK_INIT;
}
return self;
}


//卖票
- (void)sellingTickets{
os_unfair_lock_lock(&_ticketLock);

[super sellingTickets];

os_unfair_lock_unlock(&_ticketLock);
}
@end
三、递归锁 NSRecursiveLock

允许同一个线程对同一把锁进行重复加锁。要考重点同一个线程和同一把锁

-(void)NSRecursiveLockMethod
{
    NSRecursiveLock *lock = [[NSRecursiveLock alloc]init];
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        static void (^RecursiveMethod)(int);
        RecursiveMethod = ^(int value) {
            
            [lock lock];
            if (value > 0) {
                
                NSLog(@"value = %d", value);
                sleep(2);
                RecursiveMethod(value - 1);
            }
            [lock unlock];
        };
        
        RecursiveMethod(10);
    });
}

写法可以和NSLock一模一样,NSLock和NSRecursiveLock的区别
NSLock lock了之后,没有unlock那么会发生死锁。
允许同一线程多次加锁,而不会造成死锁,但是没有及时unlock,是会导致其他线程阻塞的,还是得记得unlock。

四、条件锁

根据一定条件满足后进行 加锁/解锁.

@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}
//初始化一个NSConditionLock对象
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;  //锁的条件

//满足条件时加锁
- (void)lockWhenCondition:(NSInteger)condition;


- (BOOL)tryLock;
//如果接收对象的condition与给定的condition相等,则尝试获取锁,不阻塞线程
- (BOOL)tryLockWhenCondition:(NSInteger)condition;

//解锁后,重置锁的条件
- (void)unlockWithCondition:(NSInteger)condition;

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

//在指定时间前尝试获取锁,若成功则返回YES 否则返回NO
- (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
//主线程中
    NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
    
    //线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lockWhenCondition:1];
        NSLog(@"线程1");
        sleep(2);
        [lock unlock];
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);//以保证让线程2的代码后执行
        if ([lock tryLockWhenCondition:0]) {
            NSLog(@"线程2");
            [lock unlockWithCondition:2];
            NSLog(@"线程2解锁成功");
        }
        else {
            NSLog(@"线程2尝试加锁失败");
        }
    });
    
    //线程3
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       // sleep(2);//以保证让线程2的代码后执行
        if ([lock tryLockWhenCondition:2]) {
            NSLog(@"线程3");
            [lock unlock];
            NSLog(@"线程3解锁成功");
        }
        else {
            NSLog(@"线程3尝试加锁失败");
        }
    });
    
    //线程4
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       // sleep(3);//以保证让线程2的代码后执行
        
        [lock lockWhenCondition:2];
        NSLog(@"线程4");
        [lock unlockWithCondition:1];
        NSLog(@"线程4解锁成功");
        
    });

打印结果

2020-03-28 17:53:45.896078+0800 iOSLockDemo[72828:3836240] 线程3尝试加锁失败
2020-03-28 17:53:46.900668+0800 iOSLockDemo[72828:3836241] 线程2
2020-03-28 17:53:46.900914+0800 iOSLockDemo[72828:3836241] 线程2解锁成功
2020-03-28 17:53:46.900917+0800 iOSLockDemo[72828:3836242] 线程4
2020-03-28 17:53:46.901014+0800 iOSLockDemo[72828:3836242] 线程4解锁成功
2020-03-28 17:53:46.901031+0800 iOSLockDemo[72828:3836243] 线程1

结果说明:
1 初始化一个条件锁,条件为0
2 由于线程1 和线程4条件不满足,所以循环一段时间休眠,等待满足条件满足时唤醒;线程3尝试加锁,不会阻塞线程,但是条件不满足所以直接休眠;线程2休眠1秒后尝试加锁。满足条件所以加锁成功;
3 线程2伴随重置加锁条件2进行解锁;
4 此时线程4满足条件,系统唤醒进行加锁,并且重置加锁条件14
5 此时线程1满足条件,系统唤醒进行加锁,并且解锁,此时条件为1

各种锁的性能比较

屏幕快照 2020-03-26 下午5.46.34.png
注意:
1.这个数字仅仅代表每次加解锁的耗时,并不能全方面的代表性能
2.不同的机型和系统,不同的循环次数可能结果会略微有些差异
但是还是可以看出@synchronized:是表现最差的。

五、死锁

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

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"1");// 任务1
    dispatch_sync(dispatch_get_main_queue(), ^{
       
       NSLog(@"2");// 任务2
    });
  
  NSLog(@"3");// 任务3
}

结果输出1

分析

想避免这种死锁,可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。

2、同样,下边的代码也会造成死锁:

dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{
       
        dispatch_sync(serialQueue, ^{
            
            NSLog(@"deadlock");
        });
    });

外面的函数无论是同步还是异步都会造成死锁。
**这是因为里面的任务和外面的任务都在同一个serialQueue队列内,又是同步,这就和上边主队列同步的例子一样造成了死锁
解决方法也和上边一样,将里面的同步改成异步dispatch_async,或者将serialQueue换成其他串行或并行队列,都可以解决

    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
    dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(serialQueue, ^{
       
        dispatch_sync(serialQueue2, ^{
            
            NSLog(@"deadlock");
        });
    });

这样是不会死锁的,并且serialQueue和serialQueue2是在同一个线程中的。

3、NSLock死锁

-(void)methodA
{
   self.lock = [[NSLock alloc]init];
  [self.lock lock];
  NSLog(@"1");
  [self methodB];
  NSLog(@"2");
  [self.lock unlock];
}
-(void)methodB

{
  [self.lock lock];
  NSLog(@"3");
  [self.lock unlock];
}

2020-09-25 16:40:48.177198+0800 XXX TEST  [2678:167611] 1

输出结果:1
原因如下:由于当前线程运行到第一个lock加锁,现在再次运行到lock同样的锁,需等待当前线程解锁,把当前线程挂起,不能解锁
NSLock是非递归锁,当同一线程重复获取同一非递归锁时,就会发生死锁
解决办法:
我们可以用NSRecursiveLock或者@synchronized替代NSLock
因为NSRecursiveLock是递归锁,@synchronized是同步互斥锁
递归锁:它允许同一线程多次加锁,而不会造成死锁。

-(void)methodA
{
   self.lock = [[NSLock alloc]init];
  [self.lock lock];
  NSLog(@"1");
  [self methodB];
  NSLog(@"2");
  [self.lock unlock];
}
-(void)methodB
{
    @synchronized (self) {
        
        NSLog(@"3");
    }
或
 NSRecursiveLock *lock = [[NSRecursiveLock alloc]init];
    [lock lock];
    NSLog(@"3");
   [lock unlock];

//  [self.lock lock];
//  NSLog(@"3");
//  [self.lock unlock];
}

信号量Dispatch Semaphore 也是锁

GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。
Dispatch Semaphore 提供了三个函数

Dispatch Semaphore 在实际开发中主要用于:

1、保持线程同步:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    __block NSInteger number = 0;
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        number = 100;
        
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    NSLog(@"semaphore---end,number = %zd",number);

dispatch_semaphore_wait加锁阻塞了当前线程,dispatch_semaphore_signal解锁后当前线程继续执行

可参考内容

2、保证线程安全,为线程加锁:

在线程安全中可以将dispatch_semaphore_wait看作加锁,而dispatch_semaphore_signal看作解锁
首先创建全局变量

 _semaphore = dispatch_semaphore_create(1);

注意到这里的初始化信号量是1。

- (void)asyncTask
{
    
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
    
    count++;
    
    sleep(1);
    
    NSLog(@"执行任务:%zd",count);
    
    dispatch_semaphore_signal(_semaphore);
}

异步并发调用asyncTask

for (NSInteger i = 0; i < 100; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            [self asyncTask];
        });
    }

然后发现打印是从任务1顺序执行到100,没有发生两个任务同时执行的情况。
原因如下:
在子线程中并发执行asyncTask,那么第一个添加到并发队列里的,会将信号量减1,此时信号量等于0,可以执行接下来的任务。而并发队列中其他任务,由于此时信号量不等于0,必须等当前正在执行的任务执行完毕后调用dispatch_semaphore_signal将信号量加1,才可以继续执行接下来的任务,以此类推,从而达到线程加锁的目的。

自旋锁优缺点

自旋锁的优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
自旋锁的缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用

参考一

上一篇下一篇

猜你喜欢

热点阅读