• 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];
    



           

  • 相关阅读:
    linux中关于权限的一些事
    Linux上用IP转发使内部网络连接互联网
    Linux常用基础命令
    linux路径问题
    ansible简介
    linux
    linux常用命令
    ls 命令详解
    Linux 实验 [Day 01]
    Linux SPI通过设备树文件添加设备
  • 原文地址:https://www.cnblogs.com/lxlx1798/p/10051974.html
Copyright © 2020-2023  润新知