iOS-多线程3-加锁方案2

一. NSConditionLock

NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值。

NSConditionLock相关API:

@interface NSConditionLock : NSObject <NSLocking> {

- (instancetype)initWithCondition:(NSInteger)condition;

@property (readonly) NSInteger condition; //条件值

- (void)lockWhenCondition:(NSInteger)condition; //当条件值为多少加锁,不然就一直等,等到条件值为这个值才加锁
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition; 
- (void)unlockWithCondition:(NSInteger)condition; //解锁,并把条件值置为多少
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name;

@end

简单使用如下:

#import "NSConditionLockDemo.h"

@interface NSConditionLockDemo()
@property (strong, nonatomic) NSConditionLock *conditionLock;
@end

@implementation NSConditionLockDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    }
    return self;
}

- (void)otherTest
{
    //线程1
    [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
    //线程2
    [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
    //线程3
    [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start];
}

- (void)__one
{
    [self.conditionLock lockWhenCondition:1];
    
    NSLog(@"__one");
    sleep(1);
    
    [self.conditionLock unlockWithCondition:2];
}

- (void)__two
{
    [self.conditionLock lockWhenCondition:2];
    
    NSLog(@"__two");
    sleep(1);
    
    [self.conditionLock unlockWithCondition:3];
}

- (void)__three
{
    [self.conditionLock lockWhenCondition:3];
    
    NSLog(@"__three");
    
    [self.conditionLock unlock];
}
@end

可以发现,三个子线程同时执行代码,最后打印结果是:

__one
__two
__three

先执行线程1,再执行线程2,再执行线程3,达到线程3依赖线程2,线程2依赖线程1的效果。

使用场景:

如果子线程有依赖关系(子线程的执行是有顺序的),就可以使用NSConditionLock,设置条件具体的值。

在GNUstep中查看源码:

- (id) init
{
    return [self initWithCondition: 0];
}

- (id) initWithCondition: (NSInteger)value
{
    if (nil != (self = [super init]))
    {
        if (nil == (_condition = [NSCondition new]))
        {
            DESTROY(self);
        }
        else
        {
            _condition_value = value;
            [_condition setName:
             [NSString stringWithFormat: @"condition-for-lock-%p", self]];
        }
    }
    return self;
}

可以发现两个问题:

  1. 条件值默认是0
  2. NSConditionLock的确是对NSCondition的封装

二. dispatch_queue(DISPATCH_QUEUE_SERIAL)

直接使用GCD的串行队列,也是可以实现线程同步的

#import "SerialQueueDemo.h"

@interface SerialQueueDemo()
@property (strong, nonatomic) dispatch_queue_t ticketQueue;
@property (strong, nonatomic) dispatch_queue_t moneyQueue;
@end

@implementation SerialQueueDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);
        self.moneyQueue = dispatch_queue_create("moneyQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)__drawMoney
{
    dispatch_sync(self.moneyQueue, ^{
        [super __drawMoney];
    });
}

- (void)__saveMoney
{
    dispatch_sync(self.moneyQueue, ^{
        [super __saveMoney];
    });
}

- (void)__saleTicket
{
    dispatch_sync(self.ticketQueue, ^{
        [super __saleTicket];
    });
}
@end

dispatch_sync函数的特点:要求立马在当前线程同步执行任务(当前线程是子线程,在MJBaseDemo里面已经写了)。

本来这个方法就是在子线程中执行的,把存钱、取钱操作放到一个串行队列里面,把卖票操作放到另一个串行队列里面。

举例说明:比如线程4进来卖票,那么这个操作就会被放到串行队列中,等一会线程7又进来卖票,这个操作也会被放到串行队列中,串行队列里面的东西是:线程4的卖票操作 - 线程7的卖票操作 - 线程5的卖票操作。

这样线程4卖完,线程7卖,线程7卖完,线程5卖。这样串行队列中的任务是异步的,不会出现多个线程同时访问一个成员变量的问题,这样也能解决线程安全问题。

所以说,线程同步问题也不是必须要通过加锁才能实现。

三. dispatch_semaphore

  1. semaphore叫做”信号量”
  2. 信号量的初始值,可以用来控制线程并发访问的最大数量
  3. 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

如下代码,创建15条线程,都调用test方法:

@interface SemaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@end

@implementation SemaphoreDemo
- (void)viewDidLoad {
   [super viewDidLoad]

    self.semaphore = dispatch_semaphore_create(5);

    for (int i = 0; i < 15; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
    }
}

// 线程10、7、6、9、8
- (void)test
{
    // 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
    // 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
    // 第二个参数代表等到啥时候,传入的DISPATCH_TIME_FOREVER,代表一直等
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(2);
    NSLog(@"test - %@", [NSThread currentThread]);
    
    // 让信号量的值+1
    dispatch_semaphore_signal(self.semaphore);
}
@end

打印:

test - <NSThread: 0x600003a59380>{number = 7, name = (null)}
test - <NSThread: 0x600003a59400>{number = 9, name = (null)}
test - <NSThread: 0x600003a593c0>{number = 8, name = (null)}
test - <NSThread: 0x600003a59440>{number = 10, name = (null)}
test - <NSThread: 0x600003a59140>{number = 4, name = (null)}
    间隔2秒
test - <NSThread: 0x600003a594c0>{number = 12, name = (null)}
test - <NSThread: 0x600003a59480>{number = 11, name = (null)}
test - <NSThread: 0x600003a59500>{number = 13, name = (null)}
test - <NSThread: 0x600003a59300>{number = 5, name = (null)}
test - <NSThread: 0x600003a59540>{number = 14, name = (null)}
    间隔2秒
test - <NSThread: 0x600003a59580>{number = 15, name = (null)}
test - <NSThread: 0x600003a595c0>{number = 16, name = (null)}
test - <NSThread: 0x600003a59600>{number = 17, name = (null)}
test - <NSThread: 0x600003a59340>{number = 6, name = (null)}
test - <NSThread: 0x600003a59100>{number = 3, name = (null)}

上面代码,创建了15条线程,如果不控制线程并发访问的最大数量,那么有可能15条线程同时访问test方法,这样就不安全。使用了信号量,发现最多只有5条线程访问test,这5条访问完,后面5条线程继续访问。

所以,如果信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步,代码如下:

#import "SemaphoreDemo.h"

@interface SemaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t ticketSemaphore;
@property (strong, nonatomic) dispatch_semaphore_t moneySemaphore;
@end

@implementation SemaphoreDemo

- (instancetype)init
{
    if (self = [super init]) {
        //设置信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
        self.ticketSemaphore = dispatch_semaphore_create(1);
        self.moneySemaphore = dispatch_semaphore_create(1);
    }
    return self;
}

- (void)__drawMoney
{
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
    
    [super __drawMoney];
    
    dispatch_semaphore_signal(self.moneySemaphore);
}

- (void)__saveMoney
{
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
    
    [super __saveMoney];
    
    dispatch_semaphore_signal(self.moneySemaphore);
}

- (void)__saleTicket
{
    dispatch_semaphore_wait(self.ticketSemaphore, DISPATCH_TIME_FOREVER);
    
    [super __saleTicket];
    
    dispatch_semaphore_signal(self.ticketSemaphore);
}

信号量原理:

dispatch_semaphore_t ticketSemaphore = dispatch_semaphore_create(1);
//如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
//如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
//第二个参数代表等到啥时候,传入的DISPATCH_TIME_FOREVER,代表一直等
dispatch_semaphore_wait(ticketSemaphore, DISPATCH_TIME_FOREVER);
......
//让信号量的值+1
dispatch_semaphore_signal(ticketSemaphore);

小提示:控制线程并发访问的最大数量也可以用

NSOperationQueue *queue;
queue.maxConcurrentOperationCount = 5;

四. @synchronized

  1. @synchronized是对mutex递归锁的封装
  2. @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

使用如下:

#import "SynchronizedDemo.h"

@implementation SynchronizedDemo

- (void)__drawMoney
{
    //最简单的一种方式,但是性能比较差,苹果不推荐使用,所以打出来的时候没提示。
    //其中()中是拿什么当做一把锁,比如下面是拿当前类对象当做一把锁。
    //为什么把类对象当做一把锁?因为类对象只有一个,以后无论什么实例对象调用这个方法,都是类对象作为锁,这样就只有一把锁,才能锁住。
    @synchronized([self class]) {
        [super __drawMoney];
    }
}

- (void)__saveMoney
{
    @synchronized([self class]) { // objc_sync_enter
        [super __saveMoney];
    } // objc_sync_exit
}

- (void)__saleTicket
{
    //除了用类对象作为一把锁,也可以直接传入一个其他对象,并且要dispatch_once_t,保证以后不管调用多少次都是同一个对象。
    static NSObject *lock;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        lock = [[NSObject alloc] init];
    });
    
    @synchronized(lock) {
        [super __saleTicket];
    }
}
@end

在@synchronized处打断点,查看汇编,可以发现会调用objc_sync_enter和objc_sync_exit。就相当于,第一个“{”是objc_sync_enter,第二个“}”是objc_sync_exit,如下:

@synchronized([self class]) { // objc_sync_enter
......
} // objc_sync_exit

然后在objc4中的objc-sync.mm文件中查看objc_sync_enter函数的源码实现:

typedef struct SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  
    recursive_mutex_t mutex; //递归锁
} SyncData;

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {//将传进来的obj转成data
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();//再从data里面取出一把递归锁,所以只要obj一样,取出的锁也一样
    } else {
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

从上面代码可以看出,将传进来的obj转成data,再从data里面取出一把递归锁,所以只要obj一样,取出的锁也一样,所以我们上面才说传入的obj要唯一。

递归锁:允许同一个线程对一把锁进行重复加锁(解锁)

既然@synchronized是递归锁,那么肯定可以做递归锁可以做的事情,如下:

- (void)otherTest {
    @synchronized([self class]) {
        NSLog(@"123");
        //递归调用10次
        static int count = 0;
        if (count < 10) {
            count++;
            [self otherTest];
        }
    }
}

上面代码,递归调用10次,最后打印10次“123”。如果将@synchronized换成其他锁,递归调用就会造成死锁,最后结果打印了10次,说明的确是递归锁。

注意:@synchronized和@synthesize、@dynamic不一样,别弄混淆了,关于@synthesize、@dynamic可参考Runtime3-objc_msgSend底层调用流程的补充内容。

五. 总结:回忆一下前面学的各种锁

OSSpinLock 自旋锁,因为底层是使用while循环进行忙等,不会进行休眠和唤醒,所以是性能比较高的一把锁,但是现在已经不安全,被抛弃。

os_unfair_lock 用于取代不安全的OSSpinLock ,从iOS10开始才支持。从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等,是一种互斥锁。

pthread_mutex mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。
它是跨平台的,当传入的类型是默认的就是默认锁,当传入PTHREAD_MUTEX_RECURSIVE,就是递归锁,还可以通过pthread_cond_wait(&_cond, &_mutex)当做条件锁来使用。

dispatch_semaphore 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

dispatch_queue(DISPATCH_QUEUE_SERIAL) 直接使用GCD的串行队列,也是可以实现线程同步的

NSLock是对mutex普通锁的封装
NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致
NSCondition是对mutex和cond的封装
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值

@synchronized也是对mutex递归锁的封装
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

六. 各种锁性能比较

自此,iOS中各种锁基本上都讲完了,下面比较一下性能,然后找出一个最好的,留着开发中使用。

性能从高到低排序:

os_unfair_lock iOS10开始支持
OSSpinLock 不安全,被抛弃
dispatch_semaphore 如果需要iOS8、9都支持可以使用
pthread_mutex 可以跨平台
dispatch_queue(DISPATCH_QUEUE_SERIAL) 本来GCD效率就很高
NSLock 对mutex普通锁的封装
NSCondition 对mutex和cond的封装
pthread_mutex(recursive) mutex递归锁,递归锁效率本来就低
NSRecursiveLock 对mutex递归锁的封装
NSConditionLock 对NSCondition的封装
@synchronized 对mutex递归锁的封装

总结:

一般推荐使用os_unfair_lockdispatch_semaphorepthread_mutex

使用技巧:

对于dispatch_semaphore来说,如果下面每个方法的锁都是不一样的,我们可以写成下面这样的宏:

#define SemaphoreBegin \
static dispatch_semaphore_t semaphore; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
    semaphore = dispatch_semaphore_create(1); \
}); \
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);

使用如下:

- (void)test1
{
    SemaphoreBegin;
    // .....
    SemaphoreEnd;
}

- (void)test2
{
    SemaphoreBegin;
    // .....
    SemaphoreEnd;
}

- (void)test3
{
    SemaphoreBegin;
    // .....
    SemaphoreEnd;
}

这样就保证每个方法内部的锁都不一样,同时使用起来也很简单。

如果每个方法的锁都是一样的,那就只能把锁写到外面去了。
同理,pthread_mutex也可以这样封装。

七. 面试题

  1. 你理解的多线程?
    就是多条线程同时做事情,优点就是效率高,缺点就是可能会造成线程安全问题。

  2. iOS的多线程方案有哪几种?你更倾向于哪一种?
    多线程方案如下图,更倾向于GCD。

多线程方案.png

① 这些多线程方案的底层都是依赖pthread
② NSThread线程生命周期是程序员管理,GCD和NSOperation是系统自动管理
③ NSThread和NSOperation都是OC的,更加面向对象
④ NSOperation基于CGD,使用更加面向对象

  1. 你在项目中用过 GCD 吗?

  2. GCD 的队列类型?

  3. 说一下 OperationQueue 和 GCD 的区别,以及各自的优势有哪些?

  4. 线程安全的处理手段有哪些?
    上面讲的

  5. OC你了解的锁有哪些?在你回答基础上进行二次提问;
    追问一:自旋和互斥对比?
    追问二:使用以上锁需要注意哪些?
    追问三:用C/OC/C++,任选其一,实现自旋或互斥?口述即可!

  6. 自旋锁、互斥锁比较?
    自旋锁:顾名思义就是自己一直在旋转的锁,比如上面的OSSpinLock,它底层是个while循环。
    互斥锁:互斥锁提供一个可以在同一时间,只让一个线程访问临界资源的操作接口。互斥锁(Mutex)是个提供线程同步的基本锁。上锁后,其他的线程如果想要锁上,那么会被阻塞(线程休眠),直到锁释放后(说明,一般会把访问共享内存这段代码放在上锁程序之后)———百度百科

上面讲的那些锁,除了OSSpinLock是自旋锁,其他的都是互斥锁。

  1. 什么情况使用自旋锁比较划算?
    预计线程等待锁的时间很短
    加锁的代码(临界区)经常被调用,但竞争情况很少发生
    CPU资源不紧张
    多核处理器

  2. 什么情况使用互斥锁比较划算?
    预计线程等待锁的时间较长
    单核处理器
    临界区有IO操作,因为IO操作比较占用CPU资源
    临界区代码复杂或者循环量大
    临界区竞争非常激烈

八. atomic

nonatomic和atomic
atom:原子,不可再分割的单位
atomic:原子性

原子性操作就说明这个操作是个整体,不可分割的。
比如如下三行代码,如果不是原子性操作,那么这三行代码就有可能被三个线程执行,这样肯定是不行的,那怎么变成原子性操作呢?
在第一行前面加锁,第三行后面解锁,就变成了原子性操作,变成原子性操作之后要么不执行,要执行必须把三行一块执行完,如下:

 // 加锁
 int a = 10;
 int b = 20;
 int c = a + b;
 // 解锁

atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁,也就是setter和getter内部都有加锁操作。

在objc4的objc-accessors.mm文件找到如下源码:

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) { //如果是nonatomic,就直接赋值
        oldValue = *slot;
        *slot = newValue;
    } else { //如果是atomic,就先加锁后解锁
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock(); //加锁
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock(); //解锁
    }

    objc_release(oldValue);
}

可以发现,setter方法里面,如果是nonatomic,就直接赋值,如果是atomic,就先加锁后解锁,getter方法也一样,就不解释了。

对于atomic,上面我们说了setter、getter方法内部会有加锁、解锁操作,但它并不能保证使用属性的过程是线程安全的,什么意思呢?

如下:

@interface MJPerson : NSObject
@property (strong, atomic) NSMutableArray *data; //atomic修饰
@end

//执行以下代码:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *p = [[MJPerson alloc] init];
        
        for (int i = 0; i < 10; i++) {
            dispatch_async(NULL, ^{
                p.data = [NSMutableArray array];
            });
        }
        
        NSMutableArray *array = p.data;
        // 没有加锁
        [array addObject:@"1"];
        [array addObject:@"2"];
        [array addObject:@"3"];
        // 没有解锁
    }
    return 0;
}

上面代码 p.data = [NSMutableArray array] 这一行是线程安全的,因为它的setter方法内部有加锁、解锁操作。
但是 [array addObject:@"1"] 这一行就不是线程安全的了,因为它没有加锁、解锁操作。

既然atomic是线程安全的,那么为什么开发中我们基本不用呢?

  1. 太耗性能了,因为setter、getter方法调用次数太频繁了,如果每次都需要加锁、解锁,那手机CPU资源不就被你消耗完了。所以atomic一般在MAC上才使用。

  2. 而且只有多条线程同时访问同一个对象的属性,才会有线程安全问题。这种情况几乎没有,如果你非要造出来这种情况,比如如下代码,多条线程同时访问 p.data ,那你完全可以在外面加锁嘛!

for (int i = 0; i < 10; i++) {
    dispatch_async(NULL, ^{
        // 在外面加锁
        p.data = [NSMutableArray array];
        // 在外面解锁
    });
}

九. 读写安全方案

显然,如果使用上面讲的加锁方案,那么无论读、写,同一时间只有一条线程在执行,这样效率比较低,实际上读操作可以同时多条线程一起执行的。

思考如何实现以下场景
同一时间,只能有1个线程进行写的操作
同一时间,允许有多个线程进行读的操作
同一时间,不允许既有写的操作,又有读的操作

上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有:

  1. pthread_rwlock_t:读写锁
  2. dispatch_barrier_async:异步栅栏调用

1. pthread_rwlock_t

等待锁的线程会进入休眠(有点互斥锁的感觉)

代码如下:

#import "ViewController.h"
#import <pthread.h>

@interface ViewController ()
@property (assign, nonatomic) pthread_rwlock_t lock; //读写锁
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化锁
    pthread_rwlock_init(&_lock, NULL);
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    for (int i = 0; i < 10; i++) {
        //读
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_async(queue, ^{
            [self read];
        });
        //写
        dispatch_async(queue, ^{
            [self write];
        });
        dispatch_async(queue, ^{
            [self write];
        });
        dispatch_async(queue, ^{
            [self write];
        });
    }
}


- (void)read {
    
    //读-尝试加锁
    //pthread_rwlock_tryrdlock(&_lock);
    
    //读-加锁
    pthread_rwlock_rdlock(&_lock);
    
    sleep(1);
    NSLog(@"%s--线程:%@", __func__,[NSThread currentThread]);
    //解锁
    pthread_rwlock_unlock(&_lock);
}

- (void)write
{
    //写-尝试加锁
    //pthread_rwlock_trywrlock(&_lock);
    
    //写-加锁
    pthread_rwlock_wrlock(&_lock);
    
    sleep(1);
    NSLog(@"%s--线程:%@", __func__,[NSThread currentThread]);
    
    //解锁
    pthread_rwlock_unlock(&_lock);
}

- (void)dealloc
{
    //销毁
    pthread_rwlock_destroy(&_lock);
}
@end

打印:

2021-06-01 15:41:18.160200+0800 -[read]--线程:<NSThread: 0x600000356780>{number = 4, name = (null)}
2021-06-01 15:41:18.160212+0800 -[read]--线程:<NSThread: 0x600000374a40>{number = 7, name = (null)}
2021-06-01 15:41:19.162515+0800 -[write]--线程:<NSThread: 0x600000351c80>{number = 3, name = (null)}
2021-06-01 15:41:20.165942+0800 -[read]--线程:<NSThread: 0x600000341640>{number = 5, name = (null)}
2021-06-01 15:41:21.166782+0800 -[write]--线程:<NSThread: 0x600000371a40>{number = 8, name = (null)}
2021-06-01 15:41:22.172355+0800 -[write]--线程:<NSThread: 0x600000365d80>{number = 9, name = (null)}
2021-06-01 15:41:23.176932+0800 -[read]--线程:<NSThread: 0x600000365d80>{number = 10, name = (null)}
2021-06-01 15:41:23.176946+0800 -[read]--线程:<NSThread: 0x600000372e40>{number = 11, name = (null)}
2021-06-01 15:41:23.176946+0800 -[read]--线程:<NSThread: 0x600000378e00>{number = 12, name = (null)}
2021-06-01 15:41:24.182928+0800 -[write]--线程:<NSThread: 0x600000364700>{number = 13, name = (null)}
2021-06-01 15:41:25.188879+0800 -[write]--线程:<NSThread: 0x600000365fc0>{number = 14, name = (null)}
2021-06-01 15:41:26.189801+0800 -[write]--线程:<NSThread: 0x600000364b00>{number = 15, name = (null)}
2021-06-01 15:41:27.194003+0800 -[read]--线程:<NSThread: 0x600000378dc0>{number = 18, name = (null)}
2021-06-01 15:41:27.193997+0800 -[read]--线程:<NSThread: 0x600000365dc0>{number = 17, name = (null)}
2021-06-01 15:41:27.193997+0800 -[read]--线程:<NSThread: 0x600000374480>{number = 16, name = (null)}
2021-06-01 15:41:28.194641+0800 -[write]--线程:<NSThread: 0x600000372e40>{number = 19, name = (null)}
2021-06-01 15:41:29.198154+0800 -[write]--线程:<NSThread: 0x600000365a40>{number = 20, name = (null)}
2021-06-01 15:41:30.203043+0800 -[write]--线程:<NSThread: 0x6000003746c0>{number = 21, name = (null)}
2021-06-01 15:41:31.208880+0800 -[read]--线程:<NSThread: 0x600000365ec0>{number = 22, name = (null)}
2021-06-01 15:41:31.208880+0800 -[read]--线程:<NSThread: 0x600000374ac0>{number = 24, name = (null)}
2021-06-01 15:41:31.208874+0800 -[read]--线程:<NSThread: 0x600000372940>{number = 23, name = (null)}
2021-06-01 15:41:32.209671+0800 -[write]--线程:<NSThread: 0x600000374e80>{number = 25, name = (null)}
2021-06-01 15:41:33.215514+0800 -[write]--线程:<NSThread: 0x600000374940>{number = 26, name = (null)}
2021-06-01 15:41:34.221247+0800 -[write]--线程:<NSThread: 0x600000365c00>{number = 27, name = (null)}
2021-06-01 15:41:35.227011+0800 -[read]--线程:<NSThread: 0x600000372940>{number = 29, name = (null)}
2021-06-01 15:41:35.227010+0800 -[read]--线程:<NSThread: 0x6000003746c0>{number = 28, name = (null)}
2021-06-01 15:41:35.227011+0800 -[read]--线程:<NSThread: 0x600000378e40>{number = 30, name = (null)}
2021-06-01 15:41:36.231641+0800 -[write]--线程:<NSThread: 0x600000372e00>{number = 31, name = (null)}
2021-06-01 15:41:37.237418+0800 -[write]--线程:<NSThread: 0x600000365540>{number = 32, name = (null)}
2021-06-01 15:41:38.243212+0800 -[write]--线程:<NSThread: 0x600000372fc0>{number = 33, name = (null)}

可以发现,读同时进行,写就不能同时进行了。

2. dispatch_barrier_async

废话少说,先看怎么使用,如下:

#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) dispatch_queue_t queue;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //手动创建并发队列
    self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i < 10; i++) {
        //读
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_async(self.queue, ^{
            [self read];
        });
        //写
        //当有一条线程在执行这个任务的时候,绝不允许queue中有其他线程在执行其他任务(包括上面的read和下面的write)
        dispatch_barrier_async(self.queue, ^{
            [self write];
        });
        dispatch_barrier_async(self.queue, ^{
            [self write];
        });
        dispatch_barrier_async(self.queue, ^{
            [self write];
        });
    }
}

- (void)read {
    sleep(1);
    NSLog(@"%s--线程:%@", __func__,[NSThread currentThread]);
}

- (void)write
{
    sleep(1);
    NSLog(@"%s--线程:%@", __func__,[NSThread currentThread]);
}
@end

打印如下:

2021-06-01 16:00:12.233462+0800 -[read]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:12.233472+0800 -[read]--线程:<NSThread: 0x60000144c240>{number = 3, name = (null)}
2021-06-01 16:00:12.233479+0800 -[read]--线程:<NSThread: 0x600001445d80>{number = 5, name = (null)}
2021-06-01 16:00:13.237569+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:14.239756+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:15.243263+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:16.248324+0800 -[read]--线程:<NSThread: 0x60000144c240>{number = 3, name = (null)}
2021-06-01 16:00:16.248324+0800 -[read]--线程:<NSThread: 0x600001445d80>{number = 5, name = (null)}
2021-06-01 16:00:16.248324+0800 -[read]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:17.253537+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:18.256256+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:19.257152+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:20.262030+0800 -[read]--线程:<NSThread: 0x600001445d80>{number = 5, name = (null)}
2021-06-01 16:00:20.262026+0800 -[read]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:20.262049+0800 -[read]--线程:<NSThread: 0x600001445b00>{number = 7, name = (null)}
2021-06-01 16:00:21.262937+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:22.268470+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:23.273291+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:24.279084+0800 -[read]--线程:<NSThread: 0x600001445b00>{number = 7, name = (null)}
2021-06-01 16:00:24.279085+0800 -[read]--线程:<NSThread: 0x600001445d80>{number = 5, name = (null)}
2021-06-01 16:00:24.279085+0800 -[read]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:25.281132+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:26.282865+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:27.286220+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:28.290012+0800 -[read]--线程:<NSThread: 0x600001445d80>{number = 5, name = (null)}
2021-06-01 16:00:28.290012+0800 -[read]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:28.290012+0800 -[read]--线程:<NSThread: 0x600001445b00>{number = 7, name = (null)}
2021-06-01 16:00:29.290888+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:30.296537+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:31.301259+0800 -[write]--线程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}

可以发现,打印结果和读写锁一样,读同时进行,写就不能同时进行,说明dispatch_barrier_async在文件IO操作中也是有用的。

那么是如何做到的呢?

  1. 首先,对于读操作,使用dispatch_async,所以读操作是异步的。
  2. 对于写操作,使用dispatch_barrier_async,保证当有一条线程在执行这个任务的时候,绝不允许queue中有其他线程在执行其他任务

示意图如下:

栅栏.png

注意:这个dispatch_barrier_async函数传入的并发队列必须是自己手动通过dispatch_queue_cretate创建的。如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果。

dispatch_barrier_async和dispatch_barrier_sync的区别

相同点:使用dispatch_barrier_async和dispatch_barrier_sync,都保证当有一条线程在执行这个任务的时候,绝不允许queue中有其他线程在执行其他任务。

不同点:dispatch_barrier_async是异步栅栏,会开启新线程(如上打印所示),dispatch_barrier_sync是同步栅栏,不会开启新线程。

如果把上面代码改成:

dispatch_barrier_sync(self.queue, ^{
    [self write];
});

打印如下:

2021-06-01 16:20:51.992188+0800 -[read]--线程:<NSThread: 0x6000034107c0>{number = 4, name = (null)}
2021-06-01 16:20:51.992205+0800 -[read]--线程:<NSThread: 0x6000034121c0>{number = 5, name = (null)}
2021-06-01 16:20:51.992208+0800 -[read]--线程:<NSThread: 0x600003401cc0>{number = 6, name = (null)}
2021-06-01 16:20:52.993967+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:20:53.995608+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:20:54.996736+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:20:56.001371+0800 -[read]--线程:<NSThread: 0x6000034121c0>{number = 5, name = (null)}
2021-06-01 16:20:56.001470+0800 -[read]--线程:<NSThread: 0x600003401cc0>{number = 6, name = (null)}
2021-06-01 16:20:56.001486+0800 -[read]--线程:<NSThread: 0x6000034107c0>{number = 4, name = (null)}
2021-06-01 16:20:57.002200+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:20:58.003470+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:20:59.004165+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:00.005287+0800 -[read]--线程:<NSThread: 0x600003401cc0>{number = 6, name = (null)}
2021-06-01 16:21:00.005287+0800 -[read]--线程:<NSThread: 0x6000034107c0>{number = 4, name = (null)}
2021-06-01 16:21:00.005287+0800 -[read]--线程:<NSThread: 0x6000034121c0>{number = 5, name = (null)}
2021-06-01 16:21:01.005890+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:02.006747+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:03.008123+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:04.009767+0800 -[read]--线程:<NSThread: 0x6000034121c0>{number = 5, name = (null)}
2021-06-01 16:21:04.009768+0800 -[read]--线程:<NSThread: 0x600003401cc0>{number = 6, name = (null)}
2021-06-01 16:21:04.009781+0800 -[read]--线程:<NSThread: 0x60000341fac0>{number = 3, name = (null)}
2021-06-01 16:21:05.010196+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:06.010984+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:07.011930+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:08.014189+0800 -[read]--线程:<NSThread: 0x6000034121c0>{number = 5, name = (null)}
2021-06-01 16:21:08.014189+0800 -[read]--线程:<NSThread: 0x600003401cc0>{number = 6, name = (null)}
2021-06-01 16:21:08.014190+0800 -[read]--线程:<NSThread: 0x6000034107c0>{number = 4, name = (null)}
2021-06-01 16:21:09.015798+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:10.016433+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:11.017820+0800 -[write]--线程:<NSThread: 0x600003454980>{number = 1, name = main}

可以发现,使用dispatch_barrier_sync没有开启新线程,写操作在当前线程(主线程)执行。

Demo地址:加锁方案2

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容