美文网首页
iOS多线程之线程安全<四>

iOS多线程之线程安全<四>

作者: 随风流逝 | 来源:发表于2017-08-29 15:24 被阅读31次

    多线程的使用提升了程序的性能从而提升了用户体验,但是同时也有一定的风险,那就是多个线程同时修改某一个资源造成的资源状态不确定性问题,可能我们希望在执行某个操作的过程中,只允许该操作对某个资源进行访问,所以同步工具就应运而生了。

    Atomic Operations

    原子操作是一种简单的同步形式,它适用于简单的数据类型。原子操作的优点是它们不阻塞竞争线程。简单的操作,如增加一个计数器变量,这可比走锁导致更好的性能。

    Memory Barriers and Volatile Variables

    为了达到最佳的性能,编译器经常对汇编级指令进行重新排序,使处理器的指令管道尽可能的完整。作为这种优化的一部分,编译器可能重新排序当它认为这样做不会产生错误数据时访问主存的指令。不幸的是,编译器不可能总是检测到所有与内存相关的操作。如果看似分离的变量实际上相互影响,编译器优化可以以错误的顺序更新这些变量,从而产生潜在的不正确结果。
    内存阻塞是一种非阻塞同步工具,用于确保内存操作发生在正确的顺序。内存阻塞就像一个栅栏,迫使处理器在允许执行加载和存储操作之前完成位于该栅栏前面的所有加载和存储操作。内存阻塞通常用于确保一个线程(但另一个线程可见)的内存操作总是以预期的顺序出现。在这种情况下缺乏内存阻塞可能会让其他线程看到看似不可能的结果。(例如,内存阻塞)使用内存阻塞,你只需调用OSMemoryBarrier函数在代码中相应的功能。
    Volatile变量将另一种类型的内存约束应用于单个变量。编译器通常通过将变量的值加载到寄存器中来优化代码。对于局部变量,这通常不是问题。但是,如果该变量从另一个线程可见,那么这种优化可能会阻止其他线程注意到它的任何更改。将Volatile 关键字应用于变量,迫使编译器每次使用该变量时从内存中加载该变量。如果一个变量的值可以由编译器无法检测的外部源随时更改,则可以声明该变量是不稳定的。

    Perform Selector Routines

    Cocoa applications有一种方便的方式,以同步的方式将消息传递给单个线程。NSObject类声明的方法之一,应用程序的活动线程执行一个selector。这些方法让您的线程异步传递消息,并保证它们将由目标线程同步执行。例如,您可以使用执行选择器消息将分布式计算的结果传递给应用程序的主线程或指定的协调线程。执行选择器的每个请求都在目标线程的运行循环上排队,然后按照收到的顺序依次处理这些请求。
    有关执行选择器例程的摘要和有关如何使用它们的更多信息,请参考Cocoa Perform Selector Sources

    Using Locks

    锁是线程编程的基本同步工具。锁使您能够轻松地保护大部分代码,这样您就可以确保代码的正确性。OS X和IOS为所有应用程序类型提供基本的互斥锁,基础框架为特殊情况定义了一些互斥锁的变体。下面的部分将向您展示如何使用这些锁类型。

    • @synchronized

    这个方法应该是大家最常用的一种方式,不需要我们直接创建锁,创建锁的过程被系统封装起来了,所以是开发成本最低的一种方式,但很不幸,这种方法是性能最差的,而且如果我们传入的对象是nil,那么它什么用都没有,不是线程安全的。具体原因可以看这里
    看一下@synchronized的用法和作用

    - (void)synchronizedLock {
        __block NSMutableArray *array = [[NSMutableArray alloc]initWithObjects:@"hello", @"world", nil];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            @synchronized(array) {
                sleep(2);
                NSLog(@"1-----%@", [NSThread currentThread]);
            }
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            @synchronized(array) {
                sleep(1);
                NSLog(@"2-----%@", [NSThread currentThread]);
            }
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [array addObject:@"zz"];
            @synchronized (array) {
                sleep(1);
                NSLog(@"3-----%@", [NSThread currentThread]);
            }
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSMutableArray *array1 = [array copy];
            @synchronized (array1) {
                sleep(1);
                NSLog(@"4----%@", [NSThread currentThread]);
            }
        });
    }
    

    一起看下输出结果:

    输出结果:
    2017-08-29 09:26:57.956 Lock[4367:485949] 4----<NSThread: 0x600000078880>{number = 3, name = (null)}
    2017-08-29 09:26:58.954 Lock[4367:485929] 1-----<NSThread: 0x600000265180>{number = 4, name = (null)}
    2017-08-29 09:26:59.960 Lock[4367:485932] 2-----<NSThread: 0x600000262d40>{number = 5, name = (null)}
    2017-08-29 09:27:00.965 Lock[4367:485930] 3-----<NSThread: 0x608000266700>{number = 6, name = (null)}
    

    我们代码中让1沉睡2秒,4沉睡1秒,从输出结果看两个任务是同时启动,所以4和其他不是互斥锁,而1、2、3明显就是在等待前一个任务完成才开始,所以是互斥锁,而且在3中我们是向数组中加入新元素的,因而我们得出结论,@synchronized不是根据对象的内容来的,看过分析的应该知道,@synchronized锁是根据地址来的。

    • POSIX Mutex Lock

    POSIX互斥锁的使用在任何应用程序都非常容易。创建互斥锁,你声明和初始化一个pthread_mutex_t结构。锁定和解锁的互斥锁,你用pthread_mutex_lock和pthread_mutex_unlock功能。

    - (void)posixMutex {
        __block pthread_mutex_t mutex; //声明一个pthread_mutex_t结构体
        pthread_mutex_init(&mutex, NULL);//初始化一个pthread_mutex_t结构体
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            pthread_mutex_lock(&mutex);//加锁
            NSLog(@"1---posix mutex---%@", [NSThread currentThread]);
            sleep(3);
            NSLog(@"2---posix mutex---%@", [NSThread currentThread]);
            pthread_mutex_unlock(&mutex);//解锁
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            pthread_mutex_lock(&mutex);
            NSLog(@"3---posix mutex---%@", [NSThread currentThread]);
            pthread_mutex_unlock(&mutex);
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            sleep(1);
            NSLog(@"4---posix mutex---%@", [NSThread currentThread]);
        });
    }
    
    输出信息:
    2017-08-28 17:06:01.983 Lock[2250:378898] 1---posix mutex---<NSThread: 0x608000261900>{number = 3, name = (null)}
    2017-08-28 17:06:02.983 Lock[2250:378880] 4---posix mutex---<NSThread: 0x600000260ec0>{number = 4, name = (null)}
    2017-08-28 17:06:04.988 Lock[2250:378898] 2---posix mutex---<NSThread: 0x608000261900>{number = 3, name = (null)}
    2017-08-28 17:06:04.989 Lock[2250:378899] 3---posix mutex---<NSThread: 0x60800007de00>{number = 5, name = (null)}
    

    从输出结果看到枷锁的任务是同步执行,没加锁的就是异步执行。

    • NSLock

    NSLocking

    @protocol NSLocking
    - (void)lock;
    - (void)unlock;
    @end
    

    NSLock是Cocoa提供给我们最基本的锁对象,实际上所有锁的接口(包括NSLock)是由NSLocking协议定义,它定义了锁定和解锁方法。您使用这些方法来获取和释放锁,就像任何互斥锁一样。
    除lock和unlock方法外,NSLock还提供了tryLock和lockBeforeDate:两个方法,前一个方法会尝试加锁,如果锁不可用(已经被锁住),刚并不会阻塞线程,并返回NO。lockBeforeDate:方法会在所指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。

    - (void)lock {
        NSLock *lock = [[NSLock alloc] init];//初始化一个NSLock对象
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            if ([lock tryLock]) {//尝试获取锁,如果获取不到返回NO,不会阻塞该线程
                NSLog(@"1---lock---%@", [NSThread currentThread]);
                sleep(3);
                NSLog(@"2---lock---%@", [NSThread currentThread]);
                [lock unlock];
            }else{
                NSLog(@"2---the lock is unavailable");
            }
        });
        NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:4];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            if ([lock lockBeforeDate:date]) {//在date之前加锁,如果不能返回NO
                NSLog(@"3---lock---%@", [NSThread currentThread]);
                [lock unlock];
            } else {
                NSLog(@"3---the lock is unavailable");
            }
        });
        NSDate *date2 = [[NSDate alloc] initWithTimeIntervalSinceNow:2];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            if ([lock lockBeforeDate:date2]) {//在date之前加锁,如果不能返回NO
                NSLog(@"4---lock---%@", [NSThread currentThread]);
                [lock unlock];
            } else {
                NSLog(@"4---the lock is unavailable");
            }
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            sleep(1);
            NSLog(@"5---lock---%@", [NSThread currentThread]);
        });
    }
    

    主要看3和4是什么情况

    2017-08-28 17:49:12.104 Lock[2393:417822] 1---lock---<NSThread: 0x60800006b040>{number = 3, name = (null)}
    2017-08-28 17:49:13.106 Lock[2393:417807] 5---lock---<NSThread: 0x60800006ca80>{number = 4, name = (null)}
    2017-08-28 17:49:14.108 Lock[2393:417810] 4---the lock is unavailable
    2017-08-28 17:49:15.108 Lock[2393:417822] 2---lock---<NSThread: 0x60800006b040>{number = 3, name = (null)}
    2017-08-28 17:49:15.109 Lock[2393:417808] 3---lock---<NSThread: 0x600000068200>{number = 5, name = (null)}
    

    5不重要,只是让你看一下不加锁的情况下是不会影响block的执行,因为任务是异步调度的,所以我们能从1看到任务大致的开始时间是17:49:12,所以3是试图在17:49:16之前加锁,4是试图在17:49:14之前加锁,而1把线程阻塞了3秒钟,然后其他线程才能获取锁,所以4在17:49:14的时候宣告失败,而3在17:49:15也就是1结束的时候才获取到锁。

    • NSRecursiveLock

    递归锁,一种特殊的NSLock,同一个线程可以获得多次不会导致线程死锁。递归锁跟踪它成功获取了多少次。每个成功获取锁必须通过相应的调用来解除锁的平衡。只有当所有的锁和解锁调用都平衡时,锁才会真正释放,这样其他线程就可以获得它。
    顾名思义,这种类型的锁通常用于递归函数中,以防止递归阻止线程。您也可以在非递归的情况下使用它来调用函数,它们的语义要求它们也接受锁。下面是一个通过递归获得锁的简单递归函数的示例。当你再次掉用同样的方法事,你没有调用NSRecursiveLock锁,会造成线程死锁。

    - (void)recursiveLock:(NSInteger)value {
        //初始化一个递归锁
        NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
        //加锁
        [theLock lock];
        if (value != 0) {
            NSLog(@"----%zd---", value);
            --value;
            [self recursiveLock:value];//递归调用
        }
        [theLock unlock];//解锁
    }
    
    输出结果:
    2017-08-29 10:08:22.808 Lock[4424:515786] ----5---
    2017-08-29 10:08:22.808 Lock[4424:515786] ----4---
    2017-08-29 10:08:22.809 Lock[4424:515786] ----3---
    2017-08-29 10:08:22.809 Lock[4424:515786] ----2---
    2017-08-29 10:08:22.809 Lock[4424:515786] ----1---
    

    注意:因为在所有的锁调用与解锁调用相平衡之前不会释放一个递归锁,因此您应该仔细考虑锁对效率的影响,在递归完成之前的这段内持有任何锁会导致其他线程阻塞,如果您可以重写代码以消除递归或消除使用递归锁的必要性,则可以获得更好的性能。

    • NSConditionLock

    NSConditionLock定义了一个可以锁定和解锁互斥锁的具体值。您不应该将这种类型的锁与条件(下边介绍)混淆。行为与条件有些相似,但实现的方式非常不同。
    NSConditionLock通常使用在线程需要在一个特定的顺序执行任务,例如当一个线程产生的数据,另一个线程消耗数据。当生产者正在执行时,消费者使用特定于程序的条件获取锁。当生产者完成时(条件本身是一个整数值,您定义),它打开锁并将锁定条件设置为适当的整数值来唤醒消费线程,然后处理数据。
    NSConditionLock对象能够响应加锁解锁的任意组合。例如,你可以用 lock 和 unlockWithCondition: 配对,或lockwhencondition:和 unlock 配对。当然,后一种组合方式可能不会释放等待特定条件值的线程。
    下面的示例演示如何使用条件锁处理生产者消费者问题。设想一个应用程序包含一个数据队列。生产者线程将数据添加到队列中,而消费线程从队列中提取数据。生产商不必等待特定的条件,但必须等待锁可用,以便安全地向队列中添加数据。

    - (void)conditionLock {
        NSMutableArray *products = [NSMutableArray array];
        NSInteger HAS_DATA = 1;//条件常数
        NSInteger NO_DATA = 0;//条件常数
        id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            while (YES) {
                [condLock lockWhenCondition:NO_DATA];
                [products addObject:[[NSObject alloc] init]];
                NSLog(@"product---%zi",products.count);
                sleep(1);
                [condLock unlockWithCondition:HAS_DATA];
            }
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            while (YES) {
                NSLog(@"wait for product");
                [condLock lockWhenCondition:HAS_DATA];
                [products removeObjectAtIndex:0];
                NSLog(@"product---%zi",products.count);
                BOOL isEmpty = products.count;
                [condLock unlockWithCondition:(!isEmpty ? NO_DATA : HAS_DATA)];
            }
        });
    }
    

    看输出:

    2017-08-29 11:58:59.977 Lock[4565:597516] wait for product
    2017-08-29 11:58:59.977 Lock[4565:597517] product---1
    2017-08-29 11:59:00.981 Lock[4565:597516] product---0
    2017-08-29 11:59:00.982 Lock[4565:597516] wait for product
    2017-08-29 11:59:00.982 Lock[4565:597517] product---1
    2017-08-29 11:59:01.982 Lock[4565:597516] product---0
    2017-08-29 11:59:01.983 Lock[4565:597516] wait for product
    2017-08-29 11:59:01.983 Lock[4565:597517] product---1
    2017-08-29 11:59:02.986 Lock[4565:597516] product---0
    

    初始化的时候条件为0,所以线程1顺利生产了一个产品,产品2想要获取同一个锁必须等到条件达到之后才可以,只能等待线程1解锁,之后2消耗一个产品,这之后1也要获取同一个锁也和之前2一样,需要条件达到。

    Conditions

    Conditions是一种特殊类型的锁,你可以使用它来同步操作必须执行的顺序。它们互斥的方式与互斥锁不同。等待某个条件的线程会被阻塞一直到该条件由另一个线程显式地发出信号为止。
    由于实现操作系统所涉及到的细微之处,即使它们实际上没有由你的代码发出信号条件锁也返回成功。为了避免由这些虚假信号引起的问题,你应该始终使用谓词与条件锁定相结合。谓词是确定线程是否安全进行的一种更具体的方法。该条件只会使线程处于睡眠状态,直到谓词可以由信号线程设置为止。

    • NSCondition

    NSCondition和POSIX conditions提供相同的语义,除了在单个对象中包装必需的锁和条件数据结构。结果是一个对象可以像互斥锁那样锁定,然后像条件一样等待,但是这些等待和发出信号都是需要我们手动控制。
    简单用法

    - (void)condition {
        NSCondition *condition = [[NSCondition alloc] init];
        NSMutableArray *products = [NSMutableArray array];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            while (YES) {
                [condition lock];
                if (!products.count) {
                    NSLog(@"wait for product");
                    [condition wait];//让线程处于等待
                }
                [products removeObjectAtIndex:0];
                NSLog(@"product---%zi",products.count);
                [condition unlock];
            }
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            while (YES) {
                [condition lock];
                [products addObject:[[NSObject alloc] init]];
                NSLog(@"product---%zi",products.count);
                [condition signal];//发出信号让等待的线程继续执行
                [condition unlock];
                sleep(1);
            }
        });
    }
    

    看输出结果:

    2017-08-29 14:32:36.149 Lock[5501:680404] wait for product
    2017-08-29 14:32:36.150 Lock[5501:680419] product---1
    2017-08-29 14:32:36.150 Lock[5501:680404] product---0
    2017-08-29 14:32:36.150 Lock[5501:680404] wait for product
    2017-08-29 14:32:37.155 Lock[5501:680419] product---1
    2017-08-29 14:32:37.156 Lock[5501:680404] product---0
    2017-08-29 14:32:37.156 Lock[5501:680404] wait for product
    2017-08-29 14:32:38.156 Lock[5501:680419] product---1
    2017-08-29 14:32:38.156 Lock[5501:680404] product---0
    

    产品数为0的时候没办法消耗,所以只能等待,在线程2中生产完产品之后发出一个信号告诉线程1可以继续执行,线程1消耗一个产品,然后接着依次循环。

    • POSIX Conditions

    POSIX线程状态锁需要条件的数据结构和互斥使用一起使用。虽然这两个锁结构是分开的,但是互斥锁与运行时的条件结构紧密相连。等待信号的线程应该总是使用相同的互斥锁和条件结构。更改配对会导致错误。
    经过初始化的条件与互斥锁,等待线程进入使用ready_to_go变量做谓词。只有当谓词被设置并且条件随后发出信号时,等待线程才能唤醒并开始工作。

    - (void)posixCondition {
        __block pthread_mutex_t mutex;
        __block pthread_cond_t condition;
        __block Boolean     ready_to_go = false;
        NSMutableArray *products = [NSMutableArray array];
        pthread_mutex_init(&mutex, NULL);//初始化线程互斥锁
        pthread_cond_init(&condition, NULL);//初始化条件
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            while (YES) {
                // Lock the mutex.
                pthread_mutex_lock(&mutex);
                // If the predicate is already set, then the while loop is bypassed;
                // otherwise, the thread sleeps until the predicate is set.
                while(ready_to_go == false) {
                    NSLog(@"wait for product");
                    pthread_cond_wait(&condition, &mutex);
                }
                // Do work. (The mutex should stay locked.)
                [products removeObjectAtIndex:0];
                NSLog(@"product---%zi",products.count);
                // Reset the predicate and release the mutex.
                ready_to_go = false;
                pthread_mutex_unlock(&mutex);
            }
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            while (YES) {
                // At this point, there should be work for the other thread to do.
                pthread_mutex_lock(&mutex);
                [products addObject:[[NSObject alloc] init]];
                NSLog(@"product---%zi",products.count);
                ready_to_go = true;
                // Signal the other thread to begin work.
                pthread_cond_signal(&condition);
                pthread_mutex_unlock(&mutex);
                sleep(1);
            }
        });
    }
    

    看下输出结果

    2017-08-29 15:05:28.247 Lock[5559:708783] wait for product
    2017-08-29 15:05:28.248 Lock[5559:708784] product---1
    2017-08-29 15:05:28.248 Lock[5559:708783] product---0
    2017-08-29 15:05:28.248 Lock[5559:708783] wait for product
    2017-08-29 15:05:29.250 Lock[5559:708784] product---1
    2017-08-29 15:05:29.250 Lock[5559:708783] product---0
    2017-08-29 15:05:29.251 Lock[5559:708783] wait for product
    2017-08-29 15:05:30.255 Lock[5559:708784] product---1
    2017-08-29 15:05:30.256 Lock[5559:708783] product---0
    

    和上面一样,不过换了线程锁,并且加了谓词,效果是一样的。

    dispatch_semaphore

    信号量我们在讲GCD的时候已经讲过了,还里还是再说一遍,加深一下印象

    信号量(semaphore)是非负整型变量,除了初始化之外,它只能通过两个标准原子操作:wait(semap) , signal(semap) ; 来进行访问;
    操作也被成为PV原语(P来源于Dutch proberen"测试",V来源于Dutch verhogen"增加"),而普通整型变量则可以在任何语句块中被访问;

    在GCD中有semaphore的操作:
    dispatch_semaphore_create 创建一个semaphore 
    dispatch_semaphore_signal 发送一个信号 
    dispatch_semaphore_wait 等待信号

    信号量的主要作用就是控制并发量,信号量为0则阻塞线程,大于0则不会阻塞。我们通过改变信号量的值,来控制是否阻塞线程,从而控制并发控制。
    来看个例子

    - (void)singal {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);//创建一个值为1的信号量
        NSLog(@"singal ----%zd", index);
        for (int index = 0; index < 5; index++) {
            dispatch_async(queue, ^(){
                NSLog(@"singal ----%zd", index);
                [NSThread sleepForTimeInterval:1];
                dispatch_semaphore_signal(semaphore);//信号量+1
            });
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//试图把信号量-1,如果值小于0,就等待
        }
    }
    
    看输出结果:
    2017-08-24 17:58:42.057 thread[1326:93346] singal ----1
    2017-08-24 17:58:42.057 thread[1326:93349] singal ----0
    2017-08-24 17:58:43.062 thread[1326:93346] singal ----2
    2017-08-24 17:58:43.063 thread[1326:93349] singal ----3
    2017-08-24 17:58:44.065 thread[1326:93349] singal ----4
    

    因为我们创建的信号量初始值为1,然后我们是先执行添加任务道队列,然后-1,所以我们就有两个并发。如果我们创建的信号量为0,或者我们先-1,然后添加任务道队列,那我们就只有一个并发,就等于是同步操作。

    相关文章

      网友评论

          本文标题:iOS多线程之线程安全<四>

          本文链接:https://www.haomeiwen.com/subject/nolgdxtx.html