iOS精选

OC之锁

2018-07-23  本文已影响32人  苏沫离
前言:多线程下的data race问题

demo
我们在利用并发编程以更高效率处理任务的同时,也带来了一些问题:如对共享内存的读写操作。因为要同时执行多个线程,所以整个程序的执行顺序是不同的,使用不同的执行顺序执行同一段代码得到不同的结果,这会导致并发程序中的bug难以检测和修复。
我们不妨来看一个多个线程同时修改一个数据的线程安全的例子:

- (void)testDataSafetyMethod
{
    dispatch_queue_t queue1 = dispatch_queue_create("com.demo.task1", DISPATCH_QUEUE_CONCURRENT);
    __block int length = 0;
    dispatch_apply(6, queue1, ^(size_t index) {
        dispatch_async(queue1, ^{
            NSLog(@"length === %d -- %@",++length,NSThread.currentThread);
            [NSThread sleepForTimeInterval:2];//模拟耗时任务
            NSLog(@"length --- %d == %@",--length,NSThread.currentThread);
        });
    });
}

看下它的打印结果

18:57:45.073842 length === 3 -- <NSThread: 0x60400046b2c0>{number = 6, name = (null)}
18:57:45.073842 length === 1 -- <NSThread: 0x600000467100>{number = 3, name = (null)}
18:57:45.073848 length === 4 -- <NSThread: 0x600000467780>{number = 5, name = (null)}
18:57:45.073887 length === 2 -- <NSThread: 0x60400046b0c0>{number = 4, name = (null)}
18:57:45.074246 length === 5 -- <NSThread: 0x60400046bb40>{number = 7, name = (null)}
18:57:45.074355 length === 6 -- <NSThread: 0x60400046b4c0>{number = 8, name = (null)}
18:57:47.075042 length --- 5 == <NSThread: 0x600000467100>{number = 3, name = (null)}
18:57:47.075042 length --- 5 == <NSThread: 0x60400046b2c0>{number = 6, name = (null)}
18:57:47.075042 length --- 4 == <NSThread: 0x600000467780>{number = 5, name = (null)}
18:57:47.075648 length --- 3 == <NSThread: 0x60400046b0c0>{number = 4, name = (null)}
18:57:47.075649 length --- 2 == <NSThread: 0x60400046bb40>{number = 7, name = (null)}
18:57:47.075707 length --- 1 == <NSThread: 0x60400046b4c0>{number = 8, name = (null)}

两个线程同时访问同一个变量,而且其中至少一个线程要做的是写操作,这种情况就叫竞态。Xcode 有一个线程竞态检测工具 Thread Sanitizer可以检测出这类问题。当我们开启 Thread Sanitizer时,控制台还输出了以下警告:

==================
WARNING: ThreadSanitizer: data race (pid=1083)
  Write of size 4 at 0x7b080005ce58 by thread T10:
    #0 __51-[ThreadSafetyTableController testDataSafetyMethod]_block_invoke_2 ThreadSafetyTableController.m:119 (ThreadDemo:x86_64+0x1000179d2)
    #1 __tsan::invoke_and_release_block(void*) <null>:3746304 (libclang_rt.tsan_iossim_dynamic.dylib:x86_64+0x622bb)
    #2 _dispatch_client_callout <null>:3746304 (libdispatch.dylib:x86_64+0x37eb)

  Previous write of size 4 at 0x7b080005ce58 by thread T8:
    #0 __51-[ThreadSafetyTableController testDataSafetyMethod]_block_invoke_2 ThreadSafetyTableController.m:119 (ThreadDemo:x86_64+0x1000179d2)
    #1 __tsan::invoke_and_release_block(void*) <null>:3746304 (libclang_rt.tsan_iossim_dynamic.dylib:x86_64+0x622bb)
    #2 _dispatch_client_callout <null>:3746304 (libdispatch.dylib:x86_64+0x37eb)

  Location is heap block of size 32 at 0x7b080005ce40 allocated by main thread:
    #0 malloc <null>:3746320 (libclang_rt.tsan_iossim_dynamic.dylib:x86_64+0x46fea)
    #1 _Block_object_assign <null>:3746320 (libsystem_blocks.dylib:x86_64+0xbbb)
    #2 _Block_copy <null>:3746320 (libsystem_blocks.dylib:x86_64+0x8cd)
    #3 __51-[ThreadSafetyTableController testDataSafetyMethod]_block_invoke ThreadSafetyTableController.m:118 (ThreadDemo:x86_64+0x100017946)
    #4 __wrap_dispatch_apply_block_invoke <null>:3746320 (libclang_rt.tsan_iossim_dynamic.dylib:x86_64+0x64491)
    #5 _dispatch_client_callout2 <null>:3746320 (libdispatch.dylib:x86_64+0x381c)
    #6 -[ThreadSafetyTableController testDataSafetyMethod] ThreadSafetyTableController.m:116 (ThreadDemo:x86_64+0x100017856)
    #7 -[ThreadSafetyTableController tableView:didSelectRowAtIndexPath:] ThreadSafetyTableController.m:65 (ThreadDemo:x86_64+0x1000170f9)
    #8 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] <null>:3746320 (UIKit:x86_64+0x168e88)
    #9 start <null>:3746320 (libdyld.dylib:x86_64+0x1954)

  Thread T10 (tid=36092, running) is a GCD worker thread

  Thread T8 (tid=36081, running) is a GCD worker thread

SUMMARY: ThreadSanitizer: data race ThreadSafetyTableController.m:119 in __51-[ThreadSafetyTableController testDataSafetyMethod]_block_invoke_2
==================
ThreadSanitizer report breakpoint hit. Use 'thread info -s' to get extended information about the report.

我们可以看到,程序在运行期间,报出:data race的警告。

共享内存模式需要一种机制来协调多个线程共用的数据。通常使用同步机制来实现这一目标,如锁或者判定条件:

锁是最常见的控制机制,使用它可以控制多线程对共享数据的访问。锁实施是一种互斥策略,从而避免受保护的数据和资源被多个线程同时访问。但是:使用锁协调对共享数据的访问时,很有可能引发死锁、活锁、资源匮乏等问题,这些问题导致程序中断;

苹果为我们提供了多种安全机制来让我们对共享内存进行访问:

1、互斥锁

互斥锁是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。在Objective-C种有三种互斥锁,我们来看下

1.1、@synchronized ()

@synchronized这个结构发挥了和锁一样的作用:它避免了多个线程同时执行同一段代码。和使用NSLock进行创建锁、加锁、解锁相比,在某些情况下@synchronized会更方便、更易读。

- (void)synchronizedMethod
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        NSLog(@"taskA --- 执行前");
        NSLog(@"taskA === %@",[self getTheString]);
        NSLog(@"taskA --- 执行后");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"taskB --- 执行前");
        NSLog(@"taskB === %@",[self getTheString]);
        NSLog(@"taskB --- 执行后");
    });
}

- (NSString *)getTheString
{
    @synchronized (self)
    {
        NSLog(@"currentThread == %@",NSThread.currentThread);
        [NSThread sleepForTimeInterval:2];
        return @"*****************************";
    }
}

运行程序,查看控制台输出:

 19:18:03.347253 taskA --- 执行前
 19:18:03.347261 taskB --- 执行前
 19:18:03.347567 currentThread == <NSThread: 0x60400046e600>{number = 3, name = (null)}
 19:18:05.349432 taskB === *****************************
 19:18:05.349570 currentThread == <NSThread: 0x604000471e80>{number = 4, name = (null)}
 19:18:05.349728 taskB --- 执行后
 19:18:07.351586 taskA === *****************************
 19:18:07.351929 taskA --- 执行后
1.2、NSLock

NSLock 是Cocoa提供给我们最基本的锁对象,这也是我们经常所使用的,使用POSIX线程实现其锁定行为。我们先来看下NSLock的API:

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

//NSLock在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK。
@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}
- (BOOL)tryLock; //tryLock 并不会阻塞线程。[lock tryLock] 能加锁返回 YES,不能加锁返回 NO,然后都会执行后续代码
- (BOOL)lockBeforeDate:(NSDate *)limit;//lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end

NSLock 有三种方法来执行加锁操作,那么这三个方法有何区别呢?我们使用一段代码,根据不同时间下的加锁操作,分别测试下 NSLock 的三种加锁方法的特性:

- (void)testLockMethod
{
    NSLog(@"开始处理 ---- %@",NSThread.currentThread);
    __block int length = 0;
    NSLock *lock = [[NSLock alloc] init];//初始化一个 NSLock 实例
    
    //将 taskA 放到一个并发队列中异步执行
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"taskA === 开始尝试加锁");
        [lock lock];//给 taskA 加锁
        NSLog(@"taskA === length === %d -- %@",++length,NSThread.currentThread);
        [NSThread sleepForTimeInterval:2];//模拟耗时任务
        NSLog(@"taskA === length --- %d == %@",--length,NSThread.currentThread);
        [lock unlock];//taskA 完成后解锁
        NSLog(@"taskA --- 已经解锁");
    });
    
    //将 taskB 放到一个并发队列中异步执行
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"taskB === 开始尝试加锁");
        [lock lock];//给 taskB 加锁
        NSLog(@"taskB === length === %d -- %@",++length,NSThread.currentThread);
        [NSThread sleepForTimeInterval:2];
        NSLog(@"taskB === length --- %d == %@",--length,NSThread.currentThread);
        [lock unlock];//taskB 完成后解锁
        NSLog(@"taskB --- 已经解锁");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        //tryLock 并不会阻塞线程。[lock tryLock] 能加锁返回 YES,不能加锁返回 NO,然后都会执行后续代码。
        NSLog(@"taskC === 开始尝试加锁");
        if ([lock tryLock])
        {
            NSLog(@"taskC === length === %d -- %@",++length,NSThread.currentThread);
            [NSThread sleepForTimeInterval:2];//模拟耗时任务
            NSLog(@"taskC === length --- %d == %@",--length,NSThread.currentThread);
            [lock unlock];//taskC 完成后解锁
            NSLog(@"taskC --- 已经解锁");
        }
        else
        {
            NSLog(@"加锁失败 :taskC === length --- %d == %@",length,NSThread.currentThread);
        }
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        //在所指定 Date 之前尝试加锁,会阻塞线程,如果在指定时间之前都不能加锁,则返回 NO,指定时间之前能加锁,则返回 YES。
        //taskD 尝试在 3s 之后加锁,
        NSLog(@"taskD === 开始尝试加锁");
        if ([lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:3]]){
            NSLog(@"taskD === length === %d -- %@",++length,NSThread.currentThread);
            [NSThread sleepForTimeInterval:2];
            NSLog(@"taskD === length --- %d == %@",--length,NSThread.currentThread);
            [lock unlock];
            NSLog(@"taskD --- 已经解锁");
        }
        else
        {
            NSLog(@"加锁失败 :taskD === length --- %d == %@",length,NSThread.currentThread);
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        //taskE 尝试在 10s 之后加锁 ,等待前面所有任务全部执行完毕,确保 length 资源没有被加锁
        NSLog(@"taskE === 开始尝试加锁");
        if ([lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]){
            NSLog(@"taskE === length === %d -- %@",++length,NSThread.currentThread);
            [NSThread sleepForTimeInterval:2];
            NSLog(@"taskE === length --- %d == %@",--length,NSThread.currentThread);
            [lock unlock];
            NSLog(@"taskE --- 已经解锁");
        }
        else
        {
            NSLog(@"加锁失败 :taskE === length --- %d == %@",length,NSThread.currentThread);
        }
    });
    
    //延迟 15s 执行 ,等待前面所有任务全部执行完毕,确保 length 资源没有被加锁
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(15 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"taskF === 开始尝试加锁");
        if ([lock tryLock])
        {
            NSLog(@"taskF === length === %d -- %@",++length,NSThread.currentThread);
            [NSThread sleepForTimeInterval:2];
            NSLog(@"taskF === length --- %d == %@",--length,NSThread.currentThread);
            [lock unlock];
            NSLog(@"taskF --- 已经解锁");
        }
        else
        {
            NSLog(@"加锁失败 :taskF === length --- %d == %@",length,NSThread.currentThread);
        }

    });
    
    NSLog(@"结束处理 ---- %@",NSThread.currentThread);
}

这段代码里有 taskAtaskB ... taskF 总计六个任务,我们尝试以三种方法分别为这六个任务加锁操作;

我们运行这段代码,观察输出结果:

09:40:17 taskA === 开始尝试加锁
09:40:17 taskA === length === 1 -- <NSThread: 0x600000466ec0>{number = 3, name = (null)}
09:40:17 taskB === 开始尝试加锁
09:40:17 taskC === 开始尝试加锁
09:40:17 taskD === 开始尝试加锁
09:40:17 taskE === 开始尝试加锁
09:40:17 加锁失败 :taskC === length --- 1 == <NSThread: 0x60400027fec0>{number = 4, name = (null)}
09:40:19 taskA === length --- 0 == <NSThread: 0x600000466ec0>{number = 3, name = (null)}
09:40:19 taskA --- 已经解锁
09:40:19 taskB === length === 1 -- <NSThread: 0x604000460a40>{number = 5, name = (null)}
09:40:20 加锁失败 :taskD === length --- 1 == <NSThread: 0x6040004607c0>{number = 6, name = (null)}
09:40:21 taskB === length --- 0 == <NSThread: 0x604000460a40>{number = 5, name = (null)}
09:40:21 taskB --- 已经解锁
09:40:21 taskE === length === 1 -- <NSThread: 0x60000027db00>{number = 7, name = (null)}
09:40:23 taskE === length --- 0 == <NSThread: 0x60000027db00>{number = 7, name = (null)}
09:40:23 taskE --- 已经解锁
09:40:32 taskF === 开始尝试加锁
09:40:32 taskF === length === 1 -- <NSThread: 0x600000068cc0>{number = 7, name = (null)}
09:40:34 taskF === length --- 0 == <NSThread: 0x600000068cc0>{number = 7, name = (null)}
09:40:34 taskF --- 已经解锁

从控制台输出可以看到:数据 length 在多线程中操作被加锁之后是线程安全的,这方便不过多讨论,我们主要讨论下 NSLock 的三个方法执行特点:

总结 NSLock 的三个加锁方法:

加锁方法 描述
- (void)lock; 阻塞当前线程的执行,直到前面的任务完成解锁后,才会接着执行下面代码;
- (BOOL)tryLock; 不会阻塞当前线程的执行,不能加锁直接返回 NO /能加锁直接返回 YES,然后去执行后续代码;
- (BOOL)lockBeforeDate:(NSDate *)limit; 在所指定 Date 之前尝试加锁,会阻塞线程,如果在指定时间之前都不能加锁,则返回NO,指定时间之前能加锁,则返回YES
1.3、pthread_mutex_t

pthread_mutex_t是C语言定义下多线程加锁方式,它是Unix/Linux平台上提供的一套条件互斥锁的API。使用pthread_mutex_t,需要引入头文件#include <pthread.h>pthread_mutex_t除了创建互斥锁,还可以创建递归锁、读写锁、条件锁等。
我们利用pthread_mutex_tAPI提供的函数,来实现一个互斥锁:

- (void)pthread_mutex_LockMethod
{
    NSLog(@"开始处理 ---- %@",NSThread.currentThread);
    dispatch_group_t group = dispatch_group_create();
    __block pthread_mutex_t pthreadLock = PTHREAD_MUTEX_INITIALIZER;//静态创建一个互斥锁
    __block int length = 0;
    
    //将 taskA 放到一个并发队列中异步执行
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"taskA === 开始尝试加锁");
        pthread_mutex_lock(&pthreadLock);//给 taskA 加锁
        NSLog(@"taskA === length === %d -- %@",++length,NSThread.currentThread);
        [NSThread sleepForTimeInterval:2];//模拟耗时任务
        NSLog(@"taskA === length --- %d == %@",--length,NSThread.currentThread);
        pthread_mutex_unlock(&pthreadLock);//taskA 完成后解锁
        NSLog(@"taskA --- 已经解锁");
    });
    
    //将 taskB 放到一个并发队列中异步执行
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"taskB === 开始尝试加锁");
        pthread_mutex_lock(&pthreadLock);//给 taskB 加锁
        NSLog(@"taskB === length === %d -- %@",++length,NSThread.currentThread);
        [NSThread sleepForTimeInterval:2];
        NSLog(@"taskB === length --- %d == %@",--length,NSThread.currentThread);
        pthread_mutex_unlock(&pthreadLock);//taskB 完成后解锁
        NSLog(@"taskB --- 已经解锁");
    });
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"taskC === 开始尝试加锁");
        if (pthread_mutex_trylock(&pthreadLock) == 0)
        {
            NSLog(@"taskC === length === %d -- %@",++length,NSThread.currentThread);
            [NSThread sleepForTimeInterval:2];//模拟耗时任务
            NSLog(@"taskC === length --- %d == %@",--length,NSThread.currentThread);
            pthread_mutex_unlock(&pthreadLock);//taskC 完成后解锁
            NSLog(@"taskC --- 已经解锁");
        }
        else
        {
            NSLog(@"加锁失败 :taskC === length --- %d == %@",length,NSThread.currentThread);
        }
    });
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [NSThread sleepForTimeInterval:6];//堵塞当前线程6s,等待前面所有任务全部执行完毕,确保 length 资源没有被加锁
        NSLog(@"taskD === 开始尝试加锁");
        if (pthread_mutex_trylock(&pthreadLock) == 0)
        {
            NSLog(@"taskD === length === %d -- %@",++length,NSThread.currentThread);
            [NSThread sleepForTimeInterval:2];//模拟耗时任务
            NSLog(@"taskD === length --- %d == %@",--length,NSThread.currentThread);
            pthread_mutex_unlock(&pthreadLock);//taskC 完成后解锁
            NSLog(@"taskD --- 已经解锁");
        }
        else
        {
            NSLog(@"加锁失败 :taskD === length --- %d == %@",length,NSThread.currentThread);
        }
    });
    
    //延迟 10 执行 ,等待前面所有任务全部执行完毕,确保 length 资源没有被加锁
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"taskE === 开始尝试加锁");
            if (pthread_mutex_trylock(&pthreadLock) == 0)
            {
                NSLog(@"taskE === length === %d -- %@",++length,NSThread.currentThread);
                [NSThread sleepForTimeInterval:2];//模拟耗时任务
                NSLog(@"taskE === length --- %d == %@",--length,NSThread.currentThread);
                pthread_mutex_unlock(&pthreadLock);//taskC 完成后解锁
                NSLog(@"taskE --- 已经解锁");
            }
            else
            {
                NSLog(@"加锁失败 :taskE === length --- %d == %@",length,NSThread.currentThread);
            }
        });
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"group中所有任务完成 ---- %@",NSThread.currentThread);
        pthread_mutex_destroy(&pthreadLock);
    });
}

因为 pthread_mutex 是使用 C 语言创建的,编译器并不负责 pthread_mutex 的释放,所以我们使用任务组dispatch_group,希望在任务组接收到通知后手动释放pthread_mutex
我们创建了taskAtaskB...taskE五个任务,这五个任务开始执行上锁的时间不一样,使用上锁的方式有两种:pthread_mutex_lock()pthread_mutex_trylock()
我们通过运行程序观察控制台打印,来探讨下这两种方式下加锁操作的不同。

13:58:37 开始处理 ---- <NSThread: 0x604000067c80>{number = 1, name = main}
13:58:37 taskA === 开始尝试加锁
13:58:37 taskB === 开始尝试加锁
13:58:37 taskC === 开始尝试加锁
13:58:37 taskB === length === 1 -- <NSThread: 0x60000046de40>{number = 3, name = (null)}
13:58:37 加锁失败 :taskC === length --- 1 == <NSThread: 0x60000046a240>{number = 4, name = (null)}
13:58:39 taskB === length --- 0 == <NSThread: 0x60000046de40>{number = 3, name = (null)}
13:58:39 taskB --- 已经解锁
13:58:39 taskA === length === 1 -- <NSThread: 0x60000046a200>{number = 5, name = (null)}
13:58:41 taskA === length --- 0 == <NSThread: 0x60000046a200>{number = 5, name = (null)}
13:58:41 taskA --- 已经解锁
13:58:43 taskD === 开始尝试加锁
13:58:43 taskD === length === 1 -- <NSThread: 0x600000470fc0>{number = 6, name = (null)}
13:58:45 taskD === length --- 0 == <NSThread: 0x600000470fc0>{number = 6, name = (null)}
13:58:45 taskD --- 已经解锁
13:58:45 group中所有任务完成 ---- <NSThread: 0x604000067c80>{number = 1, name = main}
13:58:47 taskE === 开始尝试加锁
13:58:47 加锁失败 :taskE === length --- 0 == <NSThread: 0x600000470fc0>{number = 6, name = (null)}
执行 方法 方法描述
静态创建 PTHREAD_MUTEX_INITIALIZER 静态创建一个互斥锁
加锁 int pthread_mutex_lock(pthread_mutex_t *) 会阻塞当前线程的执行,直到前面的任务完成解锁后,才会接着执行下面代码
加锁 int pthread_mutex_trylock(pthread_mutex_t *) 不会阻塞当前线程的执行,不能加锁直接返回 非0值 /能加锁直接返回 0,然后去执行后续代码
解锁 int pthread_mutex_unlock(pthread_mutex_t *) 解锁
销毁 int pthread_mutex_destroy(pthread_mutex_t *) 释放指定锁

使用互斥量需要注意的几点:

2、 递归锁

同一个线程可以多次加锁,不会造成死锁

2.1、 递归死锁

You should not use this class to implement a recursive lock. Calling the lock method twice on the same thread will lock up your thread permanently. Use the NSRecursiveLock class to implement recursive locks instead.

官方文档警告我们:在递归中我们不能使用NSLock,建议我们使用NSRecursiveLock。我们不妨先看下使用 NSLock在递归中运行的效果:

- (void)recursiveLockMethod
{
    NSLog(@"开始处理 ------------ %@",NSThread.currentThread);
    NSLock *lock = [[NSLock alloc] init];
    static void (^recursiveTestBlock)(int length);
    //使用 NSThread 开辟一条新线程执行递归操作处理taskA
    [NSThread detachNewThreadWithBlock:^{
        recursiveTestBlock = ^(int length)
        {
            NSLog(@"开始加锁 : taskA === length === %d -- %@",length,NSThread.currentThread);
            [lock lock];
            if (length > 0)
            {
                [NSThread sleepForTimeInterval:2];//模拟耗时任务
                recursiveTestBlock(--length);
            }
            [lock unlock];
            NSLog(@"已经解锁 : taskA --- length --- %d == %@",length,NSThread.currentThread);
        };
        recursiveTestBlock(3);
    }];
    
    //使用 NSThread 开辟一条新线程执行递归操作处理taskB
    [NSThread detachNewThreadWithBlock:^{
        NSLog(@"开始加锁 : taskB === %@",NSThread.currentThread);
        [lock lock];
        [NSThread sleepForTimeInterval:2];//模拟耗时任务
        [lock unlock];
        NSLog(@"已经解锁 : taskB === %@",NSThread.currentThread);
    }];

    NSLog(@"结束处理 ------------ %@",NSThread.currentThread);
}

下面是输出语句:

 14:07:53 开始加锁 : taskA === length === 3 -- <NSThread: 0x60400046fe40>{number = 5, name = (null)}
 14:07:53 开始加锁 : taskB === <NSThread: 0x604000474a40>{number = 6, name = (null)}
 14:07:55 开始加锁 : taskA === length === 2 -- <NSThread: 0x60400046fe40>{number = 5, name = (null)}

可以很明显的看到,线程被锁死了,代码无法接着执行;我们来分析下原因:
首先分别在两个线程里分别执行了加锁操作:从前面对 NSLock 的分析来看,taskA 加锁成功,taskB 会堵塞线程 6 ,等待线程 5 解锁后才接着执行;
然后线程 5 里,taskA 做递归,再次执行加锁操作,这时加锁不会成功,会堵塞 线程 5 直到解锁,但是代码不往下执行,就不会解锁;这样子就造成了死锁

2.2、NSRecursiveLock递归锁

我们按照苹果官方的建议使用 NSRecursiveLock 来处理上述代码:

- (void)recursiveLockMethod1
{
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
    static void (^recursiveTestBlock)(int length);
    
    //使用 NSThread 开辟一条新线程执行递归操作处理taskA
    [NSThread detachNewThreadWithBlock:^{
        
        recursiveTestBlock = ^(int length)
        {
            NSLog(@"开始加锁 : taskA === length === %d -- %@",length,NSThread.currentThread);
            [recursiveLock lock];
            if (length > 0)
            {
                [NSThread sleepForTimeInterval:2];
                recursiveTestBlock(--length);
            }
            [recursiveLock unlock];
            NSLog(@"已经解锁 : taskA --- length --- %d == %@",length,NSThread.currentThread);
        };
        recursiveTestBlock(3);
    }];
    //使用 NSThread 开辟一条新线程处理taskB
    [NSThread detachNewThreadWithBlock:^{
        NSLog(@"开始加锁 : taskB === %@",NSThread.currentThread);
        [recursiveLock lock];
        [NSThread sleepForTimeInterval:2];//模拟耗时任务
        [recursiveLock unlock];
        NSLog(@"已经解锁 : taskB === %@",NSThread.currentThread);
    }];
}

以下是输出语句:

 14:05:20 开始加锁 : taskA === length === 3 -- <NSThread: 0x60400046ff00>{number = 3, name = (null)}
 14:05:20 开始加锁 : taskB === <NSThread: 0x60000047aec0>{number = 4, name = (null)}
 14:05:22 开始加锁 : taskA === length === 2 -- <NSThread: 0x60400046ff00>{number = 3, name = (null)}
 14:05:24 开始加锁 : taskA === length === 1 -- <NSThread: 0x60400046ff00>{number = 3, name = (null)}
 14:05:26 开始加锁 : taskA === length === 0 -- <NSThread: 0x60400046ff00>{number = 3, name = (null)}
 14:05:26 已经解锁 : taskA --- length --- 0 == <NSThread: 0x60400046ff00>{number = 3, name = (null)}
 14:05:26 已经解锁 : taskA --- length --- 0 == <NSThread: 0x60400046ff00>{number = 3, name = (null)}
 14:05:26 已经解锁 : taskA --- length --- 1 == <NSThread: 0x60400046ff00>{number = 3, name = (null)}
 14:05:26 已经解锁 : taskA --- length --- 2 == <NSThread: 0x60400046ff00>{number = 3, name = (null)}
 14:05:28 已经解锁 : taskB === <NSThread: 0x60000047aec0>{number = 4, name = (null)}
2.3、利用pthread_mutex_t实现递归锁

在1.3中我们简单的了解了pthread_mutex_t互斥锁,知道了它的简单上锁/解锁的用法,现在我们再来探讨下如何利用pthread_mutex_t实现递归锁的效果:

- (void)pthread_mutex_RecursiveLockMethod
{
    __block pthread_mutex_t pthreadLock;
    pthread_mutexattr_t pthreadMutexattr;
    pthread_mutexattr_init(&pthreadMutexattr);
    pthread_mutexattr_settype(&pthreadMutexattr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&pthreadLock, &pthreadMutexattr);//初始化
    
    static void (^recursiveTestBlock)(int length);
    
    //使用 NSThread 开辟一条新线程执行递归操作处理taskA
    [NSThread detachNewThreadWithBlock:^{
        
        recursiveTestBlock = ^(int length)
        {
            NSLog(@"开始加锁 : taskA === length === %d -- %@",length,NSThread.currentThread);
            pthread_mutex_lock(&pthreadLock);//上锁
            if (length > 0)
            {
                [NSThread sleepForTimeInterval:2];
                recursiveTestBlock(--length);
            }
            pthread_mutex_unlock(&pthreadLock);//解锁
            NSLog(@"已经解锁 : taskA --- length --- %d == %@",length,NSThread.currentThread);
        };
        recursiveTestBlock(3);
    }];
    //使用 NSThread 开辟一条新线程处理taskB
    [NSThread detachNewThreadWithBlock:^{
        NSLog(@"开始加锁 : taskB === %@",NSThread.currentThread);
        pthread_mutex_lock(&pthreadLock);//上锁
        [NSThread sleepForTimeInterval:2];//模拟耗时任务
        pthread_mutex_unlock(&pthreadLock);//解锁
        NSLog(@"已经解锁 : taskB === %@",NSThread.currentThread);
    }];
}

代码和 2.2 的代码相同,只是将实现递归的锁换为pthread_mutex_t,我们来看下输出结果:

 14:56:55 开始加锁 : taskB === <NSThread: 0x604000464580>{number = 4, name = (null)}
 14:56:55 开始加锁 : taskA === length === 3 -- <NSThread: 0x60400027f780>{number = 3, name = (null)}
 14:56:57 开始加锁 : taskA === length === 2 -- <NSThread: 0x60400027f780>{number = 3, name = (null)}
 14:56:59 开始加锁 : taskA === length === 1 -- <NSThread: 0x60400027f780>{number = 3, name = (null)}
 14:57:01 开始加锁 : taskA === length === 0 -- <NSThread: 0x60400027f780>{number = 3, name = (null)}
 14:57:01 已经解锁 : taskA --- length --- 0 == <NSThread: 0x60400027f780>{number = 3, name = (null)}
 14:57:01 已经解锁 : taskA --- length --- 0 == <NSThread: 0x60400027f780>{number = 3, name = (null)}
 14:57:01 已经解锁 : taskA --- length --- 1 == <NSThread: 0x60400027f780>{number = 3, name = (null)}
 14:57:01 已经解锁 : taskA --- length --- 2 == <NSThread: 0x60400027f780>{number = 3, name = (null)}
 14:57:03 已经解锁 : taskB === <NSThread: 0x604000464580>{number = 4, name = (null)}

可以看到:pthread_mutex_t 实现了 NSRecursiveLock 相同效果的递归锁功能。
在这里,主要利用pthread_mutexattr_t实现了递归锁的功能,我们来总结下pthread_mutexattr_t的简单用法:

方法 方法描述
int pthread_mutexattr_init(pthread_mutexattr_t *) 初始化
int pthread_mutexattr_destroy(pthread_mutexattr_t *) 销毁
int pthread_mutexattr_settype(pthread_mutexattr_t *, int) 设置属性值
int pthread_mutexattr_gettype(const pthread_mutexattr_t * __restrict,int * __restrict) 获取属性值
属性值 属性值描述
PTHREAD_PROCESS_SHARE 指定pthread_mutex_t的范围:进程间的同步
PTHREAD_PROCESS_PRIVATE 指定pthread_mutex_t的范围:进程内线程间的同步(默认为进程内使用锁)
PTHREAD_MUTEX_TIMED_NP 这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性
PTHREAD_MUTEX_RECURSIVE 如果一个线程对这种类型的互斥锁重复上锁,不会引起死锁,一个线程对这类互斥锁的多次重复上锁必须由这个线程来重复相同数量的解锁,这样才能解开这个互斥锁,别的线程才能得到这个互斥锁。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码。这种类型的互斥锁只能是进程私有的(作用域属性为PTHREAD_PROCESS_PRIVATE
PTHREAD_MUTEX_RECURSIVE_NP PTHREAD_MUTEX_RECURSIVE
PTHREAD_MUTEX_ERRORCHECK 这种类型的互斥锁会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会返回一个错误代码。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码
PTHREAD_MUTEX_ERRORCHECK_NP 检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁
PTHREAD_MUTEX_ADAPTIVE_NP 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争
PTHREAD_MUTEX_NORMAL 这种类型的互斥锁不会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会引起这个线程的死锁。如果试图解锁一个由别的线程锁定的互斥锁会引发不可预料的结果。如果一个线程试图解锁已经被解锁的互斥锁也会引发不可预料的结果
PTHREAD_MUTEX_DEFAULT 这种类型的互斥锁不会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会引起不可预料的结果。如果试图解锁一个由别的线程锁定的互斥锁会引发不可预料的结果。如果一个线程试图解锁已经被解锁的互斥锁也会引发不可预料的结果。POSIX标准规定,对于某一具体的实现,可以把这种类型的互斥锁定义为其他类型的互斥锁

3、条件锁

条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行

3.1、NSConditionLock条件锁

NSConditionLock 是可以与特定的、用户定义的条件相关联的锁为条件锁。使用NSConditionLock对象,可以确保只有满足特定条件时,线程才能获得锁。一旦它获得了锁并执行了代码的关键部分,线程就可以放弃锁并将相关条件设置为新的内容。条件本身是任意的,根据应用程序的需要定义它们。
我们来看一段程序:

- (void)conditionLockMethod
{
    NSLog(@"开始处理 ------------ %@",NSThread.currentThread);
    
    __block int length = 0;
    
    //初始化一个条件锁,设置解锁条件为 1
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    
    //将 taskA 放到一个并发队列中异步执行
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"taskA === 开始尝试加锁");
        [conditionLock lock];//给 taskA 加锁
        NSLog(@"taskA === length === %d -- %@",++length,NSThread.currentThread);
        [NSThread sleepForTimeInterval:2];//模拟耗时任务
        NSLog(@"taskA === length --- %d == %@",--length,NSThread.currentThread);
        [conditionLock unlock];//taskA 完成后解锁
        NSLog(@"taskA --- 已经解锁");
    });
    
    //将 taskB 放到一个并发队列中异步执行
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"taskB === 开始尝试加锁");
        if ([conditionLock tryLockWhenCondition:1])//不会阻塞线程
        {
            NSLog(@"taskB === length === %d -- %@",++length,NSThread.currentThread);
            [NSThread sleepForTimeInterval:2];//模拟耗时任务
            NSLog(@"taskB === length --- %d == %@",--length,NSThread.currentThread);
            [conditionLock unlock];//taskB 完成后解锁
            NSLog(@"taskB --- 已经解锁");
        }
        else
        {
            NSLog(@"加锁失败 :taskB === length --- %d == %@",length,NSThread.currentThread);
        }
    });

    //将 taskC 放到一个并发队列中异步执行
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"taskC === 开始尝试加锁");
        if ([conditionLock lockWhenCondition:1 beforeDate:[NSDate dateWithTimeIntervalSinceNow:4]])
        {
            NSLog(@"taskC === length === %d -- %@",++length,NSThread.currentThread);
            [NSThread sleepForTimeInterval:2];//模拟耗时任务
            NSLog(@"taskC === length --- %d == %@",--length,NSThread.currentThread);
            [conditionLock unlockWithCondition:5];//taskC 完成后解锁
            NSLog(@"taskC --- 已经解锁");
        }
        else
        {
            NSLog(@"加锁失败 :taskC === length --- %d == %@",length,NSThread.currentThread);
        }
    });
    
    //将 taskD 放到一个并发队列中异步执行
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"taskD === 开始尝试加锁");
        [conditionLock lockWhenCondition:3];
        NSLog(@"taskD === length === %d -- %@",++length,NSThread.currentThread);
        [NSThread sleepForTimeInterval:2];//模拟耗时任务
        NSLog(@"taskD === length --- %d == %@",--length,NSThread.currentThread);
        [conditionLock unlockWithCondition:1];//taskE 完成后解锁
        NSLog(@"taskD --- 已经解锁");
    });
    
    //延迟 10s 之后执行下述任务,等待前面所有任务全部执行完毕,确保 length 资源没有被加锁
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
       
        //将 taskE 放到一个并发队列中异步执行
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"taskE === 开始尝试加锁");
            if ([conditionLock tryLockWhenCondition:3])
            {
                NSLog(@"taskE === length === %d -- %@",++length,NSThread.currentThread);
                [NSThread sleepForTimeInterval:2];//模拟耗时任务
                NSLog(@"taskE === length --- %d == %@",--length,NSThread.currentThread);
                [conditionLock unlockWithCondition:1];//taskD 完成后解锁
                NSLog(@"taskE --- 已经解锁");
            }
            else
            {
                NSLog(@"加锁失败 :taskE === length --- %d == %@",length,NSThread.currentThread);
            }
        });
        
        //将 taskF 放到一个并发队列中异步执行
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"taskF === 开始尝试加锁");
            if ([conditionLock tryLockWhenCondition:5])
            {
                NSLog(@"taskF === length === %d -- %@",++length,NSThread.currentThread);
                [NSThread sleepForTimeInterval:2];//模拟耗时任务
                NSLog(@"taskF === length --- %d == %@",--length,NSThread.currentThread);
                [conditionLock unlockWithCondition:3];//taskF 完成后解锁
                NSLog(@"taskF --- 已经解锁");
            }
            else
            {
                NSLog(@"加锁失败 :taskF === length --- %d == %@",length,NSThread.currentThread);
            }
        });        
    });
    NSLog(@"结束处理 ------------ %@",NSThread.currentThread);
}

上述代码里有 taskAtaskB ... taskF总计六个任务,我们尝试以四种方法分别为这六个任务加锁操作:

我们来看控制台输出语句:

 09:51:46 taskA === 开始尝试加锁
 09:51:46 taskB === 开始尝试加锁
 09:51:46 taskC === 开始尝试加锁
 09:51:46 taskD === 开始尝试加锁
 09:51:46 taskA === length === 1 -- <NSThread: 0x60000027a800>{number = 3, name = (null)}
 09:51:46 加锁失败 :taskB === length --- 1 == <NSThread: 0x604000663b80>{number = 4, name = (null)}
 09:51:48 taskA === length --- 0 == <NSThread: 0x60000027a800>{number = 3, name = (null)}
 09:51:48 taskA --- 已经解锁
 09:51:48 taskC === length === 1 -- <NSThread: 0x6000002795c0>{number = 5, name = (null)}
 09:51:50 taskC === length --- 0 == <NSThread: 0x6000002795c0>{number = 5, name = (null)}
 09:51:50 taskC --- 已经解锁
 09:51:56 taskE === 开始尝试加锁
 09:51:56 taskF === 开始尝试加锁
 09:51:56 加锁失败 :taskE === length --- 1 == <NSThread: 0x60400027f180>{number = 7, name = (null)}
 09:51:56 taskF === length === 1 -- <NSThread: 0x600000277a00>{number = 6, name = (null)}
 09:51:58 taskF === length --- 0 == <NSThread: 0x600000277a00>{number = 6, name = (null)}
 09:51:58 taskF --- 已经解锁
 09:51:58 taskD === length === 1 -- <NSThread: 0x604000665e00>{number = 8, name = (null)}
 09:52:00 taskD === length --- 0 == <NSThread: 0x604000665e00>{number = 8, name = (null)}
 09:52:00 taskD --- 已经解锁

数据 length 在多线程中操作被加锁之后是线程安全的,这方便不过多讨论;我们主要讨论下NSConditionLock 的几个方法执行特点:

总结 NSConditionLock 的几种加锁/解锁方法:

加锁/解锁方法 方法描述
- (void)lock; 会阻塞当前线程的执行,直到前面的任务完成解锁后,才会接着执行下面代码;
- (void)unlock; 解锁,不会改变 NSConditionLock中的condition
- (BOOL)tryLock; 不会阻塞当前线程的执行,不能加锁返回 NO /能加锁直接返回 YES,然后去执行后续代码
- (BOOL)tryLockWhenCondition:; 不会阻塞当前线程的执行,要加锁成功需要满足两个条件:其一为其余线程没有加锁,其二是condition 满足;只要满足这两条,才能加锁成功返回YES,否则返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit; 在所指定Date之前尝试加锁,会阻塞线程,如果在指定时间之前都不能加锁,则返回 NO,指定时间之前能加锁,则返回YES
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit; 在所指定 Date之前尝试加锁,会阻塞线程;如果在这个时间段内其余线程没有加锁而且满足条件 condition ,才会加锁成功;如果在等待的这段时间内:被其他线程加锁 或者 condition条件不满足,就无法加锁成功。到等待之间之后,还不行,则返回NO,加锁失败
- (void)unlockWithCondition:(NSInteger)condition; 解锁,重置 NSConditionLock 中的condition 条件
3.2、NSCondition条件锁

NSCondition 实现了一个线程在等待信号而阻塞时,可以被另外一个线程唤醒的功能。
NSCondition的对象实际上是作为一个锁和线程检查器:锁主要是为了检测条件时保护数据源,执行条件引发的任务; 线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。
NSCondition同样实现了NSLocking协议,所以它和NSLock一样,也可以调用lockunlock方法,可以当做NSLock来使用解决线程同步问题,用法完全一样。
我们不妨写个例子感受下NSCondition的用法:

- (void)conditionMethod
{
    NSCondition *condition = [[NSCondition alloc] init];
    NSMutableArray *array = [[NSMutableArray alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        
        NSLog(@"开始加锁 : taskA === %@",NSThread.currentThread);
        [condition lock];//锁定条件对象。
        [array addObject:@1];
        
        while ([array containsObject:@5] == NO)//测试是否可以安全的履行接下来的任务。
        {
            NSLog(@"执行wait: taskA === %@",NSThread.currentThread);
            //在调用 wait 方法前,必须调用 lock 给当前线程上锁
            //当一个线程执行 wait 时,NSCondition 对象 解开锁 并在此处阻塞当前线程
            //当 NSCondition 发送 signal 或者 broadcast 信号时唤醒线程
            [condition wait];//如果布尔值是假的,调用条件对象的 wait 方法来阻塞线程,不会继续执行,只有等到  signal 或 broadcast 信号,才会接着执行
            NSLog(@"接signal: taskA === %@",NSThread.currentThread);
        }
        [NSThread sleepForTimeInterval:3];//模拟耗时任务
        [condition broadcast];
        [condition unlock];//当任务完成后,解锁条件对象
        NSLog(@"已经解锁 : taskA === %@",NSThread.currentThread);
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        NSLog(@"开始加锁 : taskB === %@",NSThread.currentThread);
        [condition lock];

        while ([array containsObject:@5] == NO)
        {
            NSLog(@"执行wait: taskB === %@",NSThread.currentThread);
            [condition wait];
            NSLog(@"接signal: taskB === %@",NSThread.currentThread);
        }
        [array addObject:@2];
        [condition unlock];
        NSLog(@"已经解锁 : taskB === %@",NSThread.currentThread);
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        NSLog(@"开始加锁 : taskC === %@",NSThread.currentThread);
        [condition lock];
        
        while ([array containsObject:@5] == NO)
        {
            NSLog(@"执行wait: taskC === %@",NSThread.currentThread);
            [condition wait];
            NSLog(@"接signal: taskC === %@",NSThread.currentThread);
        }
        [array addObject:@4];
        [condition unlock];
        NSLog(@"已经解锁 : taskC === %@",NSThread.currentThread);
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        NSLog(@"开始加锁 : taskD === %@",NSThread.currentThread);
        [condition lock];
        
        while ([array containsObject:@4] == NO)
        {
            NSLog(@"执行wait: taskD === %@",NSThread.currentThread);
            if ([condition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]])
            {
                NSLog(@"接signal: taskD === %@",NSThread.currentThread);
            }
            else
            {
                NSLog(@"taskD ---- 指定时间内也没有收到 信号");
            }
        }
        [array addObject:@3];
        [condition unlock];
        NSLog(@"已经解锁 : taskD === %@",NSThread.currentThread);
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        NSLog(@"开始加锁 : taskF === %@",NSThread.currentThread);
        [condition lock];
        [array addObject:@5];
        [condition signal];
        NSLog(@"发送信号 : taskF === %@",NSThread.currentThread);
        [condition unlock];
        NSLog(@"已经解锁 : taskF === %@",NSThread.currentThread);
    });
}

上述程序执行了taskAtaskB、... taskF 六个任务:

我们来看下控制台输出:

14:25:37 开始加锁 : taskA === <NSThread: 0x60000046ac40>{number = 3, name = (null)}
14:25:37 执行wait: taskA === <NSThread: 0x60000046ac40>{number = 3, name = (null)}
14:25:38 开始加锁 : taskB === <NSThread: 0x604000279280>{number = 4, name = (null)}
14:25:38 执行wait: taskB === <NSThread: 0x604000279280>{number = 4, name = (null)}
14:25:38 开始加锁 : taskC === <NSThread: 0x600000468a80>{number = 5, name = (null)}
14:25:38 执行wait: taskC === <NSThread: 0x600000468a80>{number = 5, name = (null)}
14:25:39 开始加锁 : taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
14:25:39 执行wait: taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
14:25:40 taskD ---- 指定时间内也没有收到 信号
14:25:40 执行wait: taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
14:25:41 taskD ---- 指定时间内也没有收到 信号
14:25:41 执行wait: taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
14:25:42 taskD ---- 指定时间内也没有收到 信号
14:25:42 执行wait: taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
14:25:42 开始加锁 : taskF === <NSThread: 0x60000027b480>{number = 7, name = (null)}
14:25:42 发送信号 : taskF === <NSThread: 0x60000027b480>{number = 7, name = (null)}
14:25:44 已经解锁 : taskF === <NSThread: 0x60000027b480>{number = 7, name = (null)}
14:25:44 接signal: taskA === <NSThread: 0x60000046ac40>{number = 3, name = (null)}
14:25:47 taskD ---- 指定时间内也没有收到 信号
14:25:47 已经解锁 : taskA === <NSThread: 0x60000046ac40>{number = 3, name = (null)}
14:25:47 执行wait: taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
14:25:47 接signal: taskB === <NSThread: 0x604000279280>{number = 4, name = (null)}
14:25:47 已经解锁 : taskB === <NSThread: 0x604000279280>{number = 4, name = (null)}
14:25:47 接signal: taskC === <NSThread: 0x600000468a80>{number = 5, name = (null)}
14:25:47 已经解锁 : taskC === <NSThread: 0x600000468a80>{number = 5, name = (null)}
14:25:48 taskD ---- 指定时间内也没有收到 信号
14:25:48 已经解锁 : taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}

通过运行程序可以看到:

总结:

方法 方法描述
-(void)wait; 会使线程一直处于休眠状态,直到收到调用 -(void)signal;发出的 signal为止;
-(BOOL)waitUntilDate:(NSDate *)limit; 在使线程睡眠的同时会设置睡眠的终止时间,如果在终止时间前收到了 signal就会唤醒线程;当到达终止时间的时候,即使没有收到 signal,也会直接唤醒线程,而不会像 -(void)wait; 那样一直睡眠下去
-(void)signal; signal只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用;如果没有等待的线程,则这个方法不起作用;
-(void)broadcast; 可以唤醒所有在等待的线程。如果没有等待的线程,这个方法没有作用。
3.3、利用pthread_mutex_t实现条件锁

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

- (void)pthread_mutex_ConditionMethod
{
    __block pthread_mutex_t condMutexLock;
    __block pthread_cond_t pthreadCondition;//条件变量
    pthread_mutex_init(&condMutexLock, NULL);//初始化一个互斥锁
    pthread_cond_init(&pthreadCondition, NULL);//初始化一个条件变量
    
    NSMutableArray *array = [[NSMutableArray alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        
        NSLog(@"开始加锁 : taskA === %@",NSThread.currentThread);
        pthread_mutex_lock(&condMutexLock);//上锁
        [array addObject:@1];
        
        //wait接到signal后,并不意味着条件的值一定发生了变化,必须重新检查条件的值。最好的测试方法是循环调用:
        while ([array containsObject:@5] == NO)//测试是否可以安全的履行接下来的任务。
        {
            NSLog(@"执行wait: taskA === %@",NSThread.currentThread);
            pthread_cond_wait(&pthreadCondition, &condMutexLock);
            NSLog(@"接signal: taskA === %@",NSThread.currentThread);
        }
        [NSThread sleepForTimeInterval:3];//模拟耗时任务
        pthread_cond_broadcast(&pthreadCondition);
        pthread_mutex_unlock(&condMutexLock);//解锁
        NSLog(@"已经解锁 : taskA === %@",NSThread.currentThread);
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        NSLog(@"开始加锁 : taskB === %@",NSThread.currentThread);
        pthread_mutex_lock(&condMutexLock);//上锁

        while ([array containsObject:@5] == NO)
        {
            NSLog(@"执行wait: taskB === %@",NSThread.currentThread);
            pthread_cond_wait(&pthreadCondition, &condMutexLock);
            NSLog(@"接signal: taskB === %@",NSThread.currentThread);
        }
        [array addObject:@2];
        pthread_mutex_unlock(&condMutexLock);//解锁
        NSLog(@"已经解锁 : taskB === %@",NSThread.currentThread);
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        NSLog(@"开始加锁 : taskC === %@",NSThread.currentThread);
        pthread_mutex_lock(&condMutexLock);//上锁

        while ([array containsObject:@5] == NO)
        {
            NSLog(@"执行wait: taskC === %@",NSThread.currentThread);
            pthread_cond_wait(&pthreadCondition, &condMutexLock);
            NSLog(@"接signal: taskC === %@",NSThread.currentThread);
        }
        [array addObject:@4];
        pthread_mutex_unlock(&condMutexLock);//解锁
        NSLog(@"已经解锁 : taskC === %@",NSThread.currentThread);
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        NSLog(@"开始加锁 : taskD === %@",NSThread.currentThread);
        pthread_mutex_lock(&condMutexLock);//上锁

        while ([array containsObject:@4] == NO)
        {
            NSLog(@"执行wait: taskD === %@",NSThread.currentThread);
            struct timespec abstime;
            struct timeval now;
            long timeout_s = 1; // 等待 1s
            gettimeofday(&now, NULL);
            long nsec = now.tv_usec * 1000 + timeout_s * 1000000;
            abstime.tv_sec=now.tv_sec + nsec / 1000000000 + timeout_s;
            abstime.tv_nsec=nsec % 1000000000;
            if (pthread_cond_timedwait(&pthreadCondition, &condMutexLock, &abstime) == 0)
            {
                NSLog(@"接signal: taskD === %@",NSThread.currentThread);
            }
            else
            {
                NSLog(@"taskD ---- 指定时间内也没有收到 信号");
            }
        }
        [array addObject:@3];
        pthread_mutex_unlock(&condMutexLock);//解锁
        NSLog(@"已经解锁 : taskD === %@",NSThread.currentThread);
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        NSLog(@"开始加锁 : taskF === %@",NSThread.currentThread);
        pthread_mutex_lock(&condMutexLock);//上锁
        [array addObject:@5];
        pthread_cond_signal(&pthreadCondition);
        NSLog(@"发送信号 : taskF === %@",NSThread.currentThread);
        pthread_mutex_unlock(&condMutexLock);//解锁
        NSLog(@"已经解锁 : taskF === %@",NSThread.currentThread);
    });
}

上述程序与3.2中的程序步骤一样,只是使用互斥锁pthread_mutex_t和条件pthread_cond_t替换了 NSCondition 。此执行了taskAtaskB、... taskF六个任务:

我们来看下控制台输出:

10:16:32 开始加锁 : taskA === <NSThread: 0x60400046b4c0>{number = 3, name = (null)}
10:16:32 执行wait: taskA === <NSThread: 0x60400046b4c0>{number = 3, name = (null)}
10:16:33 开始加锁 : taskB === <NSThread: 0x60400046d080>{number = 4, name = (null)}
10:16:33 执行wait: taskB === <NSThread: 0x60400046d080>{number = 4, name = (null)}
10:16:33 开始加锁 : taskC === <NSThread: 0x600000464000>{number = 5, name = (null)}
10:16:33 执行wait: taskC === <NSThread: 0x600000464000>{number = 5, name = (null)}
10:16:34 开始加锁 : taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
10:16:34 执行wait: taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
10:16:35 taskD ---- 指定时间内也没有收到 信号
10:16:35 执行wait: taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
10:16:36 taskD ---- 指定时间内也没有收到 信号
10:16:36 执行wait: taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
10:16:37 taskD ---- 指定时间内也没有收到 信号
10:16:37 执行wait: taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
10:16:37 开始加锁 : taskF === <NSThread: 0x604000465540>{number = 7, name = (null)}
10:16:37 发送信号 : taskF === <NSThread: 0x604000465540>{number = 7, name = (null)}
10:16:37 接signal: taskA === <NSThread: 0x60400046b4c0>{number = 3, name = (null)}
10:16:37 已经解锁 : taskF === <NSThread: 0x604000465540>{number = 7, name = (null)}
10:16:40 taskD ---- 指定时间内也没有收到 信号
10:16:40 已经解锁 : taskA === <NSThread: 0x60400046b4c0>{number = 3, name = (null)}
10:16:40 执行wait: taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
10:16:41 接signal: taskB === <NSThread: 0x60400046d080>{number = 4, name = (null)}
10:16:41 已经解锁 : taskB === <NSThread: 0x60400046d080>{number = 4, name = (null)}
10:16:41 接signal: taskC === <NSThread: 0x600000464000>{number = 5, name = (null)}
10:16:41 已经解锁 : taskC === <NSThread: 0x600000464000>{number = 5, name = (null)}
10:16:42 taskD ---- 指定时间内也没有收到 信号
10:16:42 已经解锁 : taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}

通过打印可以看到:替换NSCondition之后的代码执行与使用NSCondition的代码执行,效果完全一样;

总结:

方法 方法描述
int pthread_cond_init (pthread_cond_t* cond, pthread_condattr_t *cond_attr) 初始化一个条件变量
PTHREAD_COND_INITIALIZER 静态创建一个条件变量
int pthread_cond_destroy(pthread_cond_t* cond) 销毁一个条件变量
int pthread_cond_wait(pthread_cond_t * __restrict,pthread_mutex_t * __restrict) 无条件等待;
int pthread_cond_timedwait(pthread_cond_t * __restrict, pthread_mutex_t * __restrict,const struct timespec * _Nullable __restrict) 计时等待;当在指定时间内有信号传过来时,此方法返回0,否则返回一个非0
int pthread_cond_signal(pthread_cond_t* cond) 激发条件,激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个
int pthread_cond_broadcast(pthread_cond_t* cond) 激发条件,激活所有等待线程
关于等待条件

等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待;
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()pthread_cond_timedwait()的竞争条件Race Conditionpthread_mutex_t互斥锁必须是普通锁PTHREAD_MUTEX_TIMED_NP或者适应锁PTHREAD_MUTEX_ADAPTIVE_NP,且在调用pthread_cond_wait()pthread_cond_timedwait()前必须由本线程加锁pthread_mutex_lock(),而在更新条件等待队列以前,pthread_mutex_t保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,pthread_mutex_t()将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

4、信号量

信号量的本质是数据操作锁, 它本身不具有数据交换的功能,而是通过控制其他的通信资源来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。

4.1、dispatch_semaphore信号量

dispatch_semaphoreNSCondition 类似,都是一种基于信号的同步方式。但 NSCondition 信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效);而 dispatch_semaphore 能保存发送的信号。dispatch_semaphore 的核心是 dispatch_semaphore_t类型的信号量。
我们可以使用 NSOperationQueue来直接控制线程的最大并发数量,但是我们如何在 dispatch_queue中控制线程的最大并发数?可以利用 GCD 的 dispatch_semaphore_t达到控制线程的最大并发数的目的!我们来看一段代码:

- (void)dispatch_semaphoreMethod
{
    dispatch_queue_t queue = dispatch_queue_create("com.demo.task", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
    NSLog(@"-------- 开始处理 -------");    
    dispatch_apply(6, queue, ^(size_t i) {
        long single = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        // 只有当信号量大于 0 的时候,线程将信号量减 1,程序向下执行
        // 否则线程会阻塞并且一直等待,直到信号量大于 0
        dispatch_group_async(group, queue, ^{
            NSLog(@"开始执行第 %zu 次任务 == single : %ld----- %@",i,single,NSThread.currentThread);
            [NSThread sleepForTimeInterval:(i % 2 == 0 ? 2 : 3)];//模拟耗时任务
            long value = dispatch_semaphore_signal(semaphore);// 当线程任务执行完成之后,发送一个信号,增加信号量。
            NSLog(@"结束执行第 %zu 次任务 == single : %ld----- %@",i,value,NSThread.currentThread);
        });
    });
    
    //既控制了线程数量,也在执行任务完成之后得到了通知。
    dispatch_group_notify(group, queue, ^{
        NSLog(@"任务结束 ------ %@",NSThread.currentThread);
    });
}

在上述程序中,我们利用 dispatch_apply 并发迭代了六次,向 dispatch_group 中增加了六个耗时任务;考虑到性能的优化,我们不希望无限制的开辟线程执行任务;这时可以使用 dispatch_semaphore_t 控制了线程的并发数量;
我们来看下打印结果:

10:37:32 -------- 开始处理 -------
10:37:32 开始执行第 0 次任务 == single : 0----- <NSThread: 0x60400046a280>{number = 3, name = (null)}
10:37:32 开始执行第 1 次任务 == single : 0----- <NSThread: 0x600000264380>{number = 4, name = (null)}
10:37:34 结束执行第 0 次任务 == single : 1----- <NSThread: 0x60400046a280>{number = 3, name = (null)}
10:37:34 开始执行第 2 次任务 == single : 0----- <NSThread: 0x604000462800>{number = 5, name = (null)}
10:37:35 结束执行第 1 次任务 == single : 1----- <NSThread: 0x600000264380>{number = 4, name = (null)}
10:37:35 开始执行第 3 次任务 == single : 0----- <NSThread: 0x60400046a280>{number = 3, name = (null)}
10:37:36 结束执行第 2 次任务 == single : 1----- <NSThread: 0x604000462800>{number = 5, name = (null)}
10:37:36 开始执行第 4 次任务 == single : 0----- <NSThread: 0x600000264380>{number = 4, name = (null)}
10:37:38 开始执行第 5 次任务 == single : 0----- <NSThread: 0x604000462800>{number = 5, name = (null)}
10:37:38 结束执行第 3 次任务 == single : 1----- <NSThread: 0x60400046a280>{number = 3, name = (null)}
10:37:38 结束执行第 4 次任务 == single : 0----- <NSThread: 0x600000264380>{number = 4, name = (null)}
10:37:41 结束执行第 5 次任务 == single : 0----- <NSThread: 0x604000462800>{number = 5, name = (null)}
10:37:41 任务结束 ------ <NSThread: 0x604000462800>{number = 5, name = (null)} 

我们可以看到: dispatch_group 中使用的线程只有两条。 因为我们使用 dispatch_semaphore_create() 创建了 dispatch_semaphore_t ,并赋予初值为 2

从上面的代码可以看到,一个 dispatch_semaphore_wait(signal, overTime);方法会去对应一个 dispatch_semaphore_signal(signal); 看起来像NSLocklockunlock,其实可以这样理解,区别只在于有信号量这个参数,lockunlock 只能同一时间,一个线程访问被保护的临界区,而如果 dispatch_semaphore 的信号量初始值为 value,则可以有value 个线程同时访问被保护的临界区。
总结:

dispatch_semaphore方法 方法描述
dispatch_semaphore_create(long value) 创建一个 dispatch_semaphore_t 类型的信号量,设定信号量的初始值为 value。注意:这里的传入的参数必须大于或等于 0,否则 dispatch_semaphore_create 会返回NULL
dispatch_semaphore_wait(signal, overTime); 判断signal的信号值value是否大于0。大于 0不会阻塞线程,value1,执行后续任务。如果信号值value0,该线程会和 NSCondition 一样直接进入 waiting 状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当 overTime 时限到了,也会执行后续任务。
dispatch_semaphore_signal(signal); 使value值加一,发送信号,唤醒wait中的线程。

5、读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

5.1、dispatch_barrier_async实现读写锁
- (void)dispatch_barrier_Method
{
    NSMutableArray *array = [NSMutableArray array];
    dispatch_queue_t queue = dispatch_queue_create("com.demo.barrier", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"开始执行写入操作");
    dispatch_async(queue, ^{
        NSLog(@"开始执行taskA === %@",NSThread.currentThread);
        for (int i = 0; i < 100; i ++)
        {
            [array addObject:@(i)];
        }
        [NSThread sleepForTimeInterval:3];//模拟耗时任务
        NSLog(@"结束执行taskA === %@",NSThread.currentThread);
    });
    
    //DISPATCH_BLOCK_BARRIER 保证代码块用于原子性,代码块的代码未执行结束前,下一次调用将进入一个FIFO的等待队列,等待本次代码块执行结束,使用较为安全
    dispatch_block_t taskC = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
        NSLog(@"开始执行taskC === %@",NSThread.currentThread);
        NSLog(@"taskC读取数据---- %@",array[189]);
        [NSThread sleepForTimeInterval:3];//模拟耗时任务
        NSLog(@"结束执行taskC === %@",NSThread.currentThread);
    });
    dispatch_block_t taskD = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
        NSLog(@"开始执行taskD === %@",NSThread.currentThread);
        NSLog(@"taskD读取数据---- %@",array[10]);
        [NSThread sleepForTimeInterval:3];//模拟耗时任务
        NSLog(@"结束执行taskD === %@",NSThread.currentThread);
    });
    
    NSLog(@"开始执行判定操作");
    dispatch_barrier_async(queue, ^{
        NSLog(@"开始执行taskB === %@",NSThread.currentThread);
        if (array.count < 100){
            //dispatch_block_cancel() 取消执行某个block,只有当block还未执行前执行cancel有效,block正在执行无法取消.
            dispatch_block_cancel(taskC);
            dispatch_block_cancel(taskD);
        }else if (array.count < 200){
            dispatch_block_cancel(taskC);
        }
        [NSThread sleepForTimeInterval:3];//模拟耗时任务
        NSLog(@"结束执行taskB === %@",NSThread.currentThread);
    });
    
    NSLog(@"开始执行读取操作");
    dispatch_async(queue, taskC);
    dispatch_async(queue, taskD);
    NSLog(@"结束执行读写操作");
}

我们创建了一个共享资源array,在 taskA 里对array执行写入操作;利用异步栅栏执行阻隔操作,等待taskA写入操作完成,再执行dispatch_barrier_async()block中的任务taskBtaskB是对共享资源array的判定,决定后续是否执行taskCtaskD
我们来看以下打印结果:

 10:01:10 开始执行写入操作
 10:01:10 开始执行判定操作
 10:01:10 开始执行taskA === <NSThread: 0x604000267e00>{number = 3, name = (null)}
 10:01:10 开始执行读取操作
 10:01:10 结束执行读写操作
 10:01:13 结束执行taskA === <NSThread: 0x604000267e00>{number = 3, name = (null)}
 10:01:13 开始执行taskB === <NSThread: 0x604000267e00>{number = 3, name = (null)}
 10:01:16 结束执行taskB === <NSThread: 0x604000267e00>{number = 3, name = (null)}
 10:01:16 开始执行taskD === <NSThread: 0x604000267e00>{number = 3, name = (null)}
 10:01:16 taskD读取数据---- 10
 10:01:19 结束执行taskD === <NSThread: 0x604000267e00>{number = 3, name = (null)}
5.2、dispatch_barrier_sync实现读写锁
- (void)dispatch_barrier_Method
{
    NSMutableArray *array = [NSMutableArray array];
    dispatch_queue_t queue = dispatch_queue_create("com.demo.barrier", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"开始执行写入操作");
    dispatch_async(queue, ^{
        NSLog(@"开始执行taskA === %@",NSThread.currentThread);
        for (int i = 0; i < 100; i ++)
        {
            [array addObject:@(i)];
        }
        [NSThread sleepForTimeInterval:3];//模拟耗时任务
        NSLog(@"结束执行taskA === %@",NSThread.currentThread);
    });
    
    //DISPATCH_BLOCK_BARRIER 保证代码块用于原子性,代码块的代码未执行结束前,下一次调用将进入一个FIFO的等待队列,等待本次代码块执行结束,使用较为安全
    dispatch_block_t taskC = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
        NSLog(@"开始执行taskC === %@",NSThread.currentThread);
        NSLog(@"taskC读取数据---- %@",array[189]);
        [NSThread sleepForTimeInterval:3];//模拟耗时任务
        NSLog(@"结束执行taskC === %@",NSThread.currentThread);
    });
    dispatch_block_t taskD = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
        NSLog(@"开始执行taskD === %@",NSThread.currentThread);
        NSLog(@"taskD读取数据---- %@",array[10]);
        [NSThread sleepForTimeInterval:3];//模拟耗时任务
        NSLog(@"结束执行taskD === %@",NSThread.currentThread);
    });

    NSLog(@"开始执行判定操作");
    dispatch_barrier_sync(queue, ^{
        NSLog(@"开始执行taskB === %@",NSThread.currentThread);
        if (array.count < 100){
            //dispatch_block_cancel() 取消执行某个block,只有当block还未执行前执行cancel有效,block正在执行无法取消.
            dispatch_block_cancel(taskC);
            dispatch_block_cancel(taskD);
        }else if (array.count < 200){
            dispatch_block_cancel(taskC);
        }
        [NSThread sleepForTimeInterval:3];//模拟耗时任务
        NSLog(@"结束执行taskB === %@",NSThread.currentThread);
    });
    NSLog(@"开始执行读取操作");
    dispatch_async(queue, taskC);
    dispatch_async(queue, taskD);
    NSLog(@"结束执行读写操作");
}

我们创建了一个共享资源array,在 taskA 里对array执行写入操作;利用同步栅栏执行阻隔操作,等待taskA写入操作完成,再执行dispatch_barrier_sync()block中的任务taskBtaskB是对共享资源array的判定,决定后续是否执行taskCtaskD
我们来看以下打印结果:

10:02:36 开始执行写入操作
10:02:36 开始执行判定操作
10:02:36 开始执行taskA === <NSThread: 0x600000479440>{number = 3, name = (null)}
10:02:39 结束执行taskA === <NSThread: 0x600000479440>{number = 3, name = (null)}
10:02:39 开始执行taskB === <NSThread: 0x60400006f1c0>{number = 1, name = main}
10:02:42 结束执行taskB === <NSThread: 0x60400006f1c0>{number = 1, name = main}
10:02:42 开始执行读取操作
10:02:42 结束执行读写操作
10:02:42 开始执行taskD === <NSThread: 0x600000479440>{number = 3, name = (null)}
10:02:42 taskD读取数据---- 10
10:02:45 结束执行taskD === <NSThread: 0x600000479440>{number = 3, name = (null)}
dispatch_barrier_sync()函数与dispatch_barrier_async()函数的异同点:

dispatch_barrier_sync()函数与dispatch_barrier_async()函数的相同点:

dispatch_barrier_sync()函数与dispatch_barrier_async()函数的不同点:

dispatch_barrier_sync()函数 dispatch_barrier_async()函数
堵塞当前线程 不会堵塞当前线程
不会开辟新的线程处理任务块 开辟新的线程处理任务块
5.3、pthread_rwlock_t读写锁

读写锁pthread_rwlock_t是用来解决读写操作问题的,读取数据操作可以共享,写入数据操作是排他的,读取数据可以有多个在读,,写入数据只有唯一个在写,同时写入数据的时候不允许读取数据。它具有强读者同步和强写者同步两种形式:

- (void)pthread_rwlock_Method
{
    __block pthread_rwlock_t rwlock;
    pthread_rwlock_init(&rwlock, NULL);
    NSMutableArray *array = [NSMutableArray array];
    dispatch_queue_t queue = dispatch_queue_create("com.demo.barrier", DISPATCH_QUEUE_CONCURRENT);

    //DISPATCH_BLOCK_DETACHED 不考虑线程安全
    dispatch_block_t writeTask = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
        pthread_rwlock_wrlock(&rwlock);
        NSInteger index = array.count;
        NSLog(@"开始执行writeTask:%ld === %@",index,NSThread.currentThread);
        [array addObject:@(index)];
        [NSThread sleepForTimeInterval:3];//模拟耗时任务
        NSLog(@"结束执行writeTask:%ld === %@",index,NSThread.currentThread);
        pthread_rwlock_unlock(&rwlock);
    });

    NSLog(@"开始执行写入操作");
    for (int i = 0; i < 3; i++) {
        dispatch_async(queue, writeTask);
    }
    
    NSLog(@"开始执行读取操作");
    for (NSInteger index = 0; index < 3; index++) {
        
        dispatch_async(queue, ^{
            pthread_rwlock_rdlock(&rwlock);
            NSLog(@"开始执行readTask:%ld === %@",index,NSThread.currentThread);
            NSLog(@"读取数据:%ld---- %@",index,array[index]);
            [NSThread sleepForTimeInterval:3];//模拟耗时任务
            NSLog(@"结束执行readTask:%ld === %@",index,NSThread.currentThread);
            pthread_rwlock_unlock(&rwlock);
        });
    }
    NSLog(@"结束执行读写操作");
}

我们创建了一个共享资源array,首先对array执行写入操作,加锁pthread_rwlock_wrlock(); 接着读取共享资源array,加锁pthread_rwlock_rdlock();我们来看以下打印结果:

10:24:45 开始执行写入操作
10:24:45 开始执行读取操作
10:24:45 开始执行writeTask:0 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:45 结束执行读写操作
10:24:48 结束执行writeTask:0 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:48 开始执行writeTask:1 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:51 结束执行writeTask:1 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:51 开始执行writeTask:2 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:54 结束执行writeTask:2 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:54 开始执行readTask:0 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:54 开始执行readTask:1 === <NSThread: 0x60400027cb80>{number = 4, name = (null)}
10:24:54 开始执行readTask:2 === <NSThread: 0x600000473600>{number = 5, name = (null)}
10:24:54 读取数据:0---- 0
10:24:54 读取数据:1---- 1
10:24:54 读取数据:2---- 2
10:24:57 结束执行readTask:2 === <NSThread: 0x600000473600>{number = 5, name = (null)}
10:24:57 结束执行readTask:0 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:57 结束执行readTask:1 === <NSThread: 0x60400027cb80>{number = 4, name = (null)}

通过开始执行写入操作开始执行读取操作结束执行读写操作的打印时间,可以了解到pthread_rwlock_t()不会阻塞当前线程;
通过writeTask可以看到,pthread_rwlock_wrlock()写入数据操作是排他的,只有唯一个在写,同时写入数据的时候不允许读取数据;
通过readTask可以看到,pthread_rwlock_rdlock()读取数据可以有多个在读

方法 方法描述
int pthread_rwlock_init(pthread_rwlock_t * __restrict, const pthread_rwlockattr_t * _Nullable __restrict) 初始化一个读写锁
int pthread_rwlock_destroy(pthread_rwlock_t * ) 释放指定读写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *) 读写锁 锁定 写入数据操作,会堵塞线程
int pthread_rwlock_trywrlock(pthread_rwlock_t *) 读写锁 锁定 写入数据操作,不会堵塞线程
int pthread_rwlock_rdlock(pthread_rwlock_t *) 读写锁 锁定 读取数据操作,会堵塞线程
int pthread_rwlock_tryrdlock(pthread_rwlock_t *) 读写锁 锁定 读取数据操作,不会堵塞线程
int pthread_rwlock_unlock(pthread_rwlock_t *) 解锁读写锁
5.4、互斥锁与读写锁的区别:

当访问临界区资源时(访问的含义包括所有的操作:读和写),需要上互斥锁;
当对数据(互斥锁中的临界区资源)进行读取时,需要上读取锁,当对数据进行写入时,需要上写入锁。
读写锁的优点:对于读数据比修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都要上互斥锁,而采用读写锁,则可以在任一时刻允许多个读出者存在,提高了更高的并发度,同时在某个写入者修改数据期间保护该数据,以免任何其它读出者或写入者的干扰。

6、Once操作

6.1、dispatch_once_t操作
- (void)dispatch_once_Method
{
    static id shareInstance;
    static dispatch_once_t onceToken;
    dispatch_queue_t queue = dispatch_queue_create("com.demo.task", DISPATCH_QUEUE_CONCURRENT);
    dispatch_apply(3, queue, ^(size_t index) {
        NSLog(@"开始执行:%zu ====== %@",index,NSThread.currentThread);
        dispatch_once(&onceToken, ^{
            if (!shareInstance) {
                shareInstance = [[NSObject alloc] init];
                [NSThread sleepForTimeInterval:3];
                NSLog(@"dispatch_once 执行内部 ====== %@",NSThread.currentThread);
                
            }
        });
        NSLog(@"结束执行:%zu ====== %@",index,NSThread.currentThread);
    });
}

打印结果:

14:05:17 开始执行:0 ====== <NSThread: 0x60000007d2c0>{number = 1, name = main}
14:05:17 开始执行:1 ====== <NSThread: 0x600000471440>{number = 3, name = (null)}
14:05:17 开始执行:2 ====== <NSThread: 0x60000046ed40>{number = 4, name = (null)}
14:05:20 dispatch_once 执行内部 ====== <NSThread: 0x60000007d2c0>{number = 1, name = main}
14:05:20 结束执行:0 ====== <NSThread: 0x60000007d2c0>{number = 1, name = main}
14:05:20 结束执行:1 ====== <NSThread: 0x600000471440>{number = 3, name = (null)}
14:05:20 结束执行:2 ====== <NSThread: 0x60000046ed40>{number = 4, name = (null)}

如果在一个线程调用dispatch_once()函数时,另外的线程调用dispatch_once,则调用线程等待,直到首次调用dispatch_once()的线程返回。

6.2、pthread_once_t操作
void pthread_once_Function(void) {
    static id shareInstance;
    shareInstance = [[NSObject alloc] init];
    [NSThread sleepForTimeInterval:3];
    NSLog(@"pthread_once 执行内部 ====== %@",NSThread.currentThread);
}

- (void)pthread_once_Method
{
    pthread_once_t once = PTHREAD_ONCE_INIT;
    dispatch_queue_t queue = dispatch_queue_create("com.demo.task", DISPATCH_QUEUE_CONCURRENT);
    dispatch_apply(3, queue, ^(size_t index) {
        NSLog(@"开始执行:%zu ====== %@",index,NSThread.currentThread);
        int onceValue = pthread_once(&once, &pthread_once_Function);
        NSLog(@"结束执行:%zu ====== %d",index,onceValue);
    });
}

打印结果:

 14:06:03 开始执行:0 ====== <NSThread: 0x60000007d2c0>{number = 1, name = main}
 14:06:03 开始执行:1 ====== <NSThread: 0x60400046ee40>{number = 5, name = (null)}
 14:06:03 开始执行:2 ====== <NSThread: 0x60000047ab80>{number = 6, name = (null)}
 14:06:06 pthread_once 执行内部 ====== <NSThread: 0x60000007d2c0>{number = 1, name = main}
 14:06:06 结束执行:0 ====== 0
 14:06:06 结束执行:1 ====== 0
 14:06:06 结束执行:2 ====== 0

类型为pthread_once_t的变量是一个控制变量。控制变量必须使用PTHREAD_ONCE_INIT宏静态地初始化。
pthread_once()函数首先检查控制变量,判断是否已经完成初始化,如果完成就简单地返回;否则,pthread_once调用初始化函数,并且记录下初始化被完成。如果在一个线程初始时,另外的线程调用pthread_once,则调用线程等待,直到那个线程完成初始化返回。

7、属性的atomic/nonatomic

atomic/nonatomic用来决定编译器生成的gettersetter是否为原子操作

7.1、atomic

设置成员变量的@property属性时,默认为atomic,系统会保证在其自动生成的 getter/setter 方法中的操作是完整的,不受其他线程的影响。例如线程5在执行 getter 方法时,线程6执行了 setter 方法,此时 线程5依然会得到一个完整无损的对象。

//声明一个atomic修饰的属性
@property (atomic ,copy) NSString *testStrig;

//setter 方法的内部实现
 - (void)setTestStrig:(NSString *)testStrig
 {
     {lock}
     if (![_testStrig isEqualToString:testStrig])
     {
        _testStrig = testStrig;
     }
    {unlock}
 }

atomic不是线程安全的,如果有另一个线程9 同时在调[testStrig release],那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。

atomic nonatomic
默认修饰符 不是默认的
读写速度慢,性能差 速度更快,提高性能
读写安全,线程不安全 读写不安全,线程不安全

demo

参考文章:
iOS 常见知识点(三):Lock
C语言互斥锁pthread_mutex_t
iOS 中常见的几种锁
pthread_cond_wait()用法分析
pthread_rwlock_t读写锁函数说明
ios atomic nonatomic区别

上一篇下一篇

猜你喜欢

热点阅读