iOS 线程安全--锁

Posted 梁飞宇

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 线程安全--锁相关的知识,希望对你有一定的参考价值。

一,前言

  线程安全是ios开发中避免了的话题,随着多线程的使用,对于资源的竞争以及数据的操作都可能存在风险,所以有必要在操作时保证线程安全。

二,为什么要使用锁?

     由于一个进程中不可避免的存在多线程,所以不可避免的存在多个线程访问同一个数据的情况。但是为了数据的安全性,当一个线程访问数据的时候,其它的线程不能对其访问。简单来讲就是在同一时刻,对同一个数据操作的线程只有一个。只有确保了这样,才能使数据不会被其他线程影响。而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。例如,一个内存单元存储着一个可读,可写的变量数据10,我们想取到10时,另外一个线程把它改成11,就会造成我们取到的数据,并不是我们想要的。再比如,写文件和读文件,当一个线程在写文件的时候,理论上来说,如果这个时候另一个线程来直接读取的话,那么得到的结果可能是你无法预料的。

示例:我们定义一个person类,创建一个NSInterge  age的属性,开辟两个线程去改变age的值。
    

- (void)withoutLock {
    __block Person *p = [[Person alloc]init];
    
    [NSThread detachNewThreadWithBlock:^{ //开辟一个新线程
        for (int i = 0; i<1000; i++) {
            p.age ++;
        }
        NSLog(@"%zd
",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{ //开辟一个新线程
        for (int i = 0; i<1000; i++) {
            p.age ++;
        }
        NSLog(@"%zd
",p.age);
    }];
    
}

 打印结果:

2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339611] 1893
2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339610] 1012

 分析结果:

   按正常的理想情况,打印的结果应该为1000,2000; 造成这个问题的主要原因就是我们开辟的两个线程都去访问age的内存单元,造成数据混乱。

 假如我们加上锁以后:
 

- (void)useLock {
    __block Person *p = [[Person alloc]init];
    
    NSLock *myLock = [[NSLock alloc]init];
    NSLog(@"begin:");
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i<1000; i++) {
            [myLock lock]; //加锁
             p.age ++;
            [myLock unlock]; //解锁
        }
        NSLog(@"%zd
",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i<1000; i++) {
            [myLock lock]; //加锁
            p.age ++;
            [myLock unlock]; //解锁
        }
        NSLog(@"%zd
",p.age);
    }];
}

 打印结果:

2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339611] 1000
2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339610] 2000

三,怎么保证线程安全

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

四,常用的锁有哪些

  •  NSLock 
  •  Synchronized 同步锁
  •  Atomic 自旋锁
  •  Recursivelock 递归锁
  •  Dispatch_semaphore 信号量
  •  NSConditionLock和NSCondition 条件锁

五,常用锁的使用

  • NSLock

         * 系统API

@protocol NSLocking
lock 方法
- (void)lock //获得锁
unlock 方法
- (void)unlock //释放锁

 

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}
- (BOOL)tryLock; //试图得到一个锁。YES:成功得到锁;NO:没有得到锁。
- (BOOL)lockBeforeDate:(NSDate *)limit; //在指定的时间以前得到锁。YES:在指定时间之前获得了锁;NO:在指定时间之前没有获得锁。该线程将被阻塞,直到获得了锁,或者指定时间过期。
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); //给锁定义一个Name
@end     

   * NSLock的执行原理

   某个线程A调用lock方法。这样,NSLock将被上锁。可以执行“关键部分”,完成后,调用unlock方法。如果,在线程A 调用unlock方法之前,另一个线程B调用了同一锁对象的lock方法。那么,线程B只有等待。直到线程A调用了unlock。
       * 使用方法

//初始化数据锁(主线程中)
NSLock *lock =[NSLock alloc]init];
//数据加锁
[lock lock];

//加锁的内容
[object doSomeThine];
//数据解锁 [lock Unlock];

       * 使用示例

//主线程中
NSLock *lock = [[NSLock alloc] init];

//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  [lock lock];
  NSLog(@"线程1");
  sleep(2);
  [lock unlock];
  NSLog(@"线程1解锁成功");
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  sleep(1);//以保证让线程2的代码后执行
  [lock lock];
  NSLog(@"线程2");
  [lock unlock];
});

 结果:

2018-12-02 14:23:09.659 ThreadLockControlDemo[1754:129663] 线程1
2018-12-02 14:23:11.663 ThreadLockControlDemo[1754:129663] 线程1解锁成功
2018-12-02 14:23:11.665 ThreadLockControlDemo[1754:129659] 线程2

    * 注意事项

Warning
 * NSLock类使用POSIX(可移植性操作系统接口)线程来实现上锁的特性。当NSLock类收到一个解锁的消息,你必须确定发送源也是来自那个发送上锁的线程。在不同的线程上解锁,会产生不定义行为。

 * 你不应该把这个类实现递归锁。如果在同一个线程上调用两次lock方法,将会对这个线程永久上锁。使用NSRecursiveLock类来才可以实现递归锁。
 * 解锁一个没有被锁定的锁是一个程序错误,这个地方需要注意。
  •  Synchronized 同步锁

   同步锁是比较常用的,因为其使用方法是所有锁中最简单的,但性能却是最差的,所以对性能要求不高的使用场景Synchronized是一种比较方便的锁。

         * 使用示例:

static Config * instance = nil;
//方法A
+(Config *) Instance {

    @synchronized(self)  {

        if(nil == instance)  {
            [self new];
        }
    }
    return instance;
}

//方法B
+(id)allocWithZone:(NSZone *)zone {

    @synchronized(self)  {
        if(instance == nil){
            instance = [super allocWithZone:zone];
            return instance;
        }
    }
    return nil;
}

         * 使用介绍:

        @synchronized,代表这个方法加锁, 相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程例如B正在用这个方法,有的话要等正在使用synchronized方法的线程B运行完这个方法后再运行此线程A,没有的话,直接运行。它包括两种用法:synchronized 方法和 synchronized 块。

      @synchronized 方法控制对类(一般在IOS中用在单例中)的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法锁方能执行,否则所属就会发生线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类,至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突(只要所有可能访问类的方法均被声明为 synchronized)。

    synchronized 块:

     @通过 synchronized关键字来声明synchronized 块。语法如下:

     @synchronized(syncObject) {

     }

      synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。

         * 使用总结

  1. 从上可以看出不需要创建锁,一种类似于swift中调用一个含有尾随闭包的函数,就能实现功能。
  2. synchronized内部实现是对传入的对象,为其分配一个递归锁,存储在哈希表中。

         * 使用注意:
            @synchronized(){} 小括号里面需要传入一个对象类型,基本数据类型不能作为参数;

            @synchronized(){}小括号内的这个对象不能为空,如果为nil,就不能保证其锁的功能。

  • Atomic 自旋锁

         自旋锁在iOS系统中的实现是OSSpinLock。自旋锁通过一直处于while盲等状态,来实现只有一个线程访问数据。由于一直处于while循环,所以对CPU的占用也比较高的,用CPU的消耗换来的好处就是自旋锁的性能高。

         * 使用介绍

            当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(busy-waiting),当上一个线程的任务执行完毕,下一个线程会立即执行。

         * 优缺点

      1. 由于自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁

                2. 自旋锁会一直占用CPU,也可能会造成死锁

                3.自旋锁有bug!不同优先级线程调度算法会有优先级反转问题,比如低优先级获锁访问资源,高优先级尝试访问时会等待,这时低优先级又没法争过高优先级导致任务无法完成lock释放不了

         * 原子操作

  • nonatomic:非原子属性,非线程安全,适合小内存移动设备
  • atomic:原子属性,default,线程安全(内部使用自旋锁),消耗大量资源
    • 单写多读,只为setter方法加锁,不影响getter

    • 相关代码如下:

      static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) 
      {
         if (offset == 0) {
             object_setClass(self, newValue);
             return;
         }
      
         id oldValue;
         id *slot = (id*) ((char*)self + offset);
      
         if (copy) {
             newValue = [newValue copyWithZone:nil];
         } else if (mutableCopy) {
             newValue = [newValue mutableCopyWithZone:nil];
         } else {
             if (*slot == newValue) return;
             newValue = objc_retain(newValue);
         }
      
         if (!atomic) {
             oldValue = *slot;
             *slot = newValue;
         } else {
             spinlock_t& slotlock = PropertyLocks[slot];
             slotlock.lock();
             oldValue = *slot;
             *slot = newValue;        
             slotlock.unlock();
         }
      
         objc_release(oldValue);
      }
      
      void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
      {
         bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
         bool mutableCopy = (shouldCopy == MUTABLE_COPY);
         reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
      }
      
      
    • 总结:很容易理解的代码,可变拷贝和不可变拷贝会开辟新的空间,两者皆不是则持有(引用计数+1),相比nonatomic只是多了一步锁操作。   

           * 使用示例

#import "ViewController.h"
#import "Person.h"
#import <libkern/OSAtomic.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self  useLock];
}

- (void)withoutLock {
    __block Person *p = [[Person alloc]init];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i<1000; i++) {
            p.age ++;
        }
        NSLog(@"%zd
",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        @synchronized(self){
            
        }
        for (int i = 0; i<1000; i++) {
            p.age ++;
        }
        NSLog(@"%zd
",p.age);
    }];
    
}

- (void)useLock {
    
    __block OSSpinLock spinLock = OS_SPINLOCK_INIT; //创建锁
    __block Person *p = [[Person alloc]init];
    
    
    NSLog(@"begin:");
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i<1000; i++) {
            OSSpinLockLock(&spinLock); //加锁
             p.age ++;
            OSSpinLockLock(&spinLock); //解锁
        }
        NSLog(@"%zd
",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i<1000; i++) {
            OSSpinLockLock(&spinLock); //加锁
            p.age ++;
            OSSpinLockLock(&spinLock); //解锁
        }
        NSLog(@"%zd
",p.age);
    }];
}

            * 使用总结

               1)首先需要#important<libkern/OSAtomic.h> ,因此关于自旋锁的API是在这个文件中声明的。

               2)创建自旋锁也是通过一个静态宏,在线程内通过 OSSpinLockLock 和 OSSpinLockUnlock来上锁,解锁。如果不是因为现在的OSSpinLock出现了使用bug,在性能以及使用方面来说,都是很好的使用锁的选择。

            * 自旋锁的原理

     就是while循环来占用CPU,实际上,当A线程获取到锁时,CPU会处于while死循环,而这个死循环并不是A线程造成的,当A获取到锁,并且B线程也要申请锁时,就会一直while循环询问A线程是否释放了该锁,所以导致了CPU死循环,因此是B线程导致的,这个是“自旋”的由来,正是因为这个一直等待询问,并不类似于互斥锁,互斥锁在申请时处于线程休眠状态,所以才使自旋锁的性能高。举个列子:煮饭吃,你的电饭锅(A线程)正在煮饭(资源),而你本人(B线程)也想煮饭,你有两种方式,第一种,一直在电饭锅前等待着,看着饭好了没;第二种,去忙其它的,每15分钟过来看一次饭好了没。很显然,按照第一种方式肯定是会先吃上饭。

  • Recursivelock 递归锁

        * 需求场景

     一个锁只是请求一份资源,而在一些开发实际中,往往需要在代码中嵌套锁的使用,也就是在同一个线程中,一个锁还没有解锁就再次加锁。这个时候就用到了递归锁。

        * 实现原理

    递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型。NSRecursiveLock 与 NSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE

        * 运用场景

    循环(多张图片循环上传),递归

        * 使用示例
 示例一:   

//递归锁实例化

    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    static void (^RecursiveMethod)(NSInteger);
    //  同一线程可多次加锁,不会造成死锁
    RecursiveMethod = ^(NSInteger value){
        [lock lock];//一进来就要开始加锁
        [NetWorkManager requestWithMethod:POST Url:url Parameters:paraDic success:^(id responseObject) {
            [self reuestForSuccess];
  //一旦数据获取成功就要解锁 不然会造成死锁
              [lock unlock];
        } requestRrror:^(id requestRrror) {
            //条件没有达到,开始循环操作
            if(value > 0){
                RecursiveMethod(value-1);//必须-1  循环
            }

            if(value == 0){
            //条件 如果 == 0 代表循环的次数条件已经达到 可以做别的操作
            }

            //失败后也要解锁
                [lock unlock];
        }];

             //记得解锁
              [lock unlock];
    };

    //设置递归锁循环次数  自定义
       RecursiveMethod(5);

 示例二:

- (void)recursiveLock {
    NSRecursiveLock *theLock = [[NSRecursiveLock alloc]init];
    [self MyRecursiveFucntion:5 recursiveLock:theLock];  
}
- (void) MyRecursiveFucntion:(NSInteger )value recursiveLock:(NSRecursiveLock *)theLock { [theLock lock]; if (value !=0) { --value; [self MyRecursiveFucntion:value recursiveLock:theLock]; } [theLock unlock]; }
  • Dispatch_semaphore 信号量

    dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是

    dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。

下面我们逐一介绍三个函数:

  (1)dispatch_semaphore_create的声明为:

     dispatch_semaphore_t  dispatch_semaphore_create(long value);

     传入的参数为long,输出一个dispatch_semaphore_t类型且值为value的信号量。

    值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。

    (关于信号量,我就不在这里累述了,网上很多介绍这个的。我们这里主要讲一下dispatch_semaphore这三个函数的用法)。

  (2)dispatch_semaphore_signal的声明为:

      long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

      这个函数会使传入的信号量dsema的值加1;(至于返回值,待会儿再讲)

    (3) dispatch_semaphore_wait的声明为:

    long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

    这个函数会使传入的信号量dsema的值减1;

    这个函数的作用是这样的,如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;

    如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,

    不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,

    且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。

    如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。

  (4)dispatch_semaphore_signal的返回值为long类型,当返回值为0时表示当前并没有线程等待其处理的信号量,其处理

     的信号量的值加1即可。当返回值不为0时,表示其当前有(一个或多个)线程等待其处理的信号量,并且该函数唤醒了一

    个等待的线程(当线程有优先级时,唤醒优先级最高的线程;否则随机唤醒)。

    dispatch_semaphore_wait的返回值也为long型。当其返回0时表示在timeout之前,该函数所处的线程被成功唤醒。

    当其返回不为0时,表示timeout发生。

  (5)在设置timeout时,比较有用的两个宏:DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER。

     DISPATCH_TIME_NOW  表示当前;

     DISPATCH_TIME_FOREVER  表示遥远的未来;

    一般可以直接设置timeout为这两个宏其中的一个,或者自己创建一个dispatch_time_t类型的变量。

    创建dispatch_time_t类型的变量有两种方法,dispatch_time和dispatch_walltime。

    利用创建dispatch_time创建dispatch_time_t类型变量的时候一般也会用到这两个变量。

    dispatch_time的声明如下:

      dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);

      其参数when需传入一个dispatch_time_t类型的变量,和一个delta值。表示when加delta时间就是timeout的时间。

    例如:dispatch_time_t  t = dispatch_time(DISPATCH_TIME_NOW, 1*1000*1000*1000);

       表示当前时间向后延时一秒为timeout的时间。

    (6)关于信号量,一般可以用停车来比喻。

    停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。

    信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait函数就相当于来了一辆车,dispatch_semaphore_signal

    就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),

    调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait剩余车位就减少一个;

    当剩余车位为0时,再来车(即调用dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主

    没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,

    所以就一直等下去。

  (7)代码举简单示例如下:

 
   dispatch_semaphore_t signal;
    signal = dispatch_semaphore_create(1);
    __block long x = 0;
    NSLog(@"0_x:%ld",x);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        NSLog(@"waiting");
        x = dispatch_semaphore_signal(signal);
        NSLog(@"1_x:%ld",x);
 
        sleep(2);
        NSLog(@"waking");
        x = dispatch_semaphore_signal(signal);
        NSLog(@"2_x:%ld",x);
    });
//    dispatch_time_t duration = dispatch_time(DISPATCH_TIME_NOW, 1*1000*1000*1000); //超时1秒
//    dispatch_semaphore_wait(signal, duration);
 
    x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
    NSLog(@"3_x:%ld",x);
 
    x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
    NSLog(@"wait 2");
    NSLog(@"4_x:%ld",x);
 
    x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
    NSLog(@"wait 3");
    NSLog(@"5_x:%ld",x);

最终打印的结果为:

 
2018-12-02 22:51:54.734 LHTest[15700:70b] 0_x:0
2018-12-02 22:51:54.737 LHTest[15700:70b] 3_x:0
2018-12-02 22:51:55.738 LHTest[15700:f03] waiting
2018-12-02 22:51:55.739 LHTest[15700:70b] wait 2
2018-12-02 22:51:55.739 LHTest[15700:f03] 1_x:1
2018-12-02 22:51:55.739 LHTest[15700:70b] 4_x:0
2018-12-02 22:51:57.741 LHTest[15700:f03] waking
2018-12-02 22:51:57.742 LHTest[15700:f03] 2_x:1
2018-12-02 22:51:57.742 LHTest[15700:70b] wait 3
2018-12-02 22:51:57.742 LHTest[15700:70b] 5_x:0
  •  NSConditionLock和NSCondition 条件锁

    * 使用介绍
    NSConditionLock好处是可以设置条件,条件符合时获得锁。设置时间,指定时间之前获取锁。缺点是加锁和解锁需要在同一线程中执行,否则控制台会报错,虽然不影响程序运行。(but好像会影响进程释放,因为多次执行后进程到了80多,程序卡了还是崩溃了,忘了。只是猜测。)

      * 使用举例

 NSConditionLock * conditionLock = [[NSConditionLockalloc] init];
      //当条件符合时获得锁
      [conditionLock lockWhenCondition:1];
     //在指定时间前尝试获取锁,若成功则返回YES 否则返回NO
      BOOL isLock = [conditionLock lockBeforeDate:date1];
    //在指定时间前尝试获取锁,且条件必须符合
      BOOL isLock = [conditionLock lockWhenCondition:1 beforeDate:date1];
    //解锁并设置条件为2
     [conditionLock unlockWithCondition:2];

 



 

       

 
















以上是关于iOS 线程安全--锁的主要内容,如果未能解决你的问题,请参考以下文章

iOS 线程安全--锁

iOS - 互斥锁&&自旋锁 多线程安全隐患(转载)

IOS多线程安全(线程锁)

iOS线程同步(各种锁)

[iOS开发]iOS中的相关锁

[iOS开发]iOS中的相关锁