iOS锁--八大锁

969 阅读23分钟

一、@synchronized

锁是最常用的同步工具。一段代码段在同一个时间只能允许被有限个线程访问,比如一个线程 A 进入需要保护代码之前添加简单的互斥锁,另一个线程 B 就无法访问,只有等待前一个线程 A 执行完被保护的代码后解锁,B 线程才能访问被保护代码。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.ticketCount = 20;
    [self lg_testSaleTicket];
}
- (void)lg_testSaleTicket{

    for (int i = 0; i < 3; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (int i = 0; i < 3; i++) {
                [self saleTicket];
            }
        });
    }
}

- (void)saleTicket{
    // 枷锁 - 线程安全
    @synchronized (self) {
        if (self.ticketCount > 0) {
            self.ticketCount--;
            sleep(0.1);
            NSLog(@"当前余票还剩:%ld张",self.ticketCount);
        }else{
            NSLog(@"当前车票已售罄");
        }
    }

}

输出结果

2020-03-03 20:02:28.275159+0800 001-@synchronized分析[85117:4216359] 当前余票还剩:19张
2020-03-03 20:02:28.275355+0800 001-@synchronized分析[85117:4216359] 当前余票还剩:18张
2020-03-03 20:02:28.275593+0800 001-@synchronized分析[85117:4216359] 当前余票还剩:17张
2020-03-03 20:02:28.275720+0800 001-@synchronized分析[85117:4216358] 当前余票还剩:16张
2020-03-03 20:02:28.275830+0800 001-@synchronized分析[85117:4216358] 当前余票还剩:15张
2020-03-03 20:02:28.275947+0800 001-@synchronized分析[85117:4216358] 当前余票还剩:14张
2020-03-03 20:02:28.277016+0800 001-@synchronized分析[85117:4216356] 当前余票还剩:13张
2020-03-03 20:02:28.277139+0800 001-@synchronized分析[85117:4216356] 当前余票还剩:12张
2020-03-03 20:02:28.277616+0800 001-@synchronized分析[85117:4216356] 当前余票还剩:11张

上面代码,假如我们在saleTicket方法中没有添加@synchronized输出就是:

2020-03-03 20:04:06.822674+0800 001-@synchronized分析[85214:4219574] 当前余票还剩:19张
2020-03-03 20:04:06.822721+0800 001-@synchronized分析[85214:4219575] 当前余票还剩:18张
2020-03-03 20:04:06.822852+0800 001-@synchronized分析[85214:4219580] 当前余票还剩:17张
2020-03-03 20:04:06.822936+0800 001-@synchronized分析[85214:4219575] 当前余票还剩:15张
2020-03-03 20:04:06.822935+0800 001-@synchronized分析[85214:4219574] 当前余票还剩:15张
2020-03-03 20:04:06.822959+0800 001-@synchronized分析[85214:4219580] 当前余票还剩:14张
2020-03-03 20:04:06.823059+0800 001-@synchronized分析[85214:4219575] 当前余票还剩:13张
2020-03-03 20:04:06.823085+0800 001-@synchronized分析[85214:4219580] 当前余票还剩:12张
2020-03-03 20:04:06.823091+0800 001-@synchronized分析[85214:4219574] 当前余票还剩:11张

里面会发现,第15张票造成了资源抢夺现象,这种情况就叫做线程不安全,当加锁以后就买票顺序就很正常了。

下面我们来分析下@synchronized是如何对代码进行加锁的,首先我们打开汇编,然后在@synchronized地方加一个断点:

我们在汇编里面发下了objc_sync_enter这个函数

并且在下面发现了objc_sync_exit函数

我们猜测在objc_sync_enter和objc_sync_exit之间就是加锁的一块代码

我们在main.m中添加@synchronized,然后通过clang命令将.m转成.cpp文件,

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

然后我们打开.cpp文件,我们发现被编译成这个代码

我们知道加锁代码是在objc_sync_enter和objc_sync_exit之间执行的,我们可以查看一下这两个函数的具体实现,通过打符号断点查看一下这两个函数实现在哪

我们发现objc_sync_enter其实是在libobjc.dylib库里面,也就是objc的源码,我们打开objc的源码搜索一下objc_sync_enter,找到它的具体实现

根据上面注释我们知道@synchronized是基于obj的递归锁

  • 其实递归锁是一种特殊的互斥锁

根据实现我们发现当obj为nil时候,会调用objc_sync_nil();,我们再看一下**objc_sync_nil();**的实现,

BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);

并且根据注释** @synchronized(nil) does nothing**我们知道当obj为nil的时候其实什么事情都没做,所以我们得出结论,

当@synchronized(nil){}时候,其实是没有加锁成功的,所以在@synchronized(obj)时候,我们要保证obj没有被释放

当obj有值时候,我们先看一下SyncData的数据结构,

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;//类似于子类
    DisguisedPtr<objc_object> object;//锁住的对象
    int32_t threadCount;  // number of THREADS using this block----线程号
    recursive_mutex_t mutex;//递归锁
} SyncData;

因为mutex是一个递归锁,所以搭配objc_sync_nil就可以避免出现死锁的产生

我们再看一下id2data的实现

我们查看一下LOCK_FOR_OBJ的实现

// Use multiple parallel lists to decrease contention among unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data

static StripedMap<SyncList> sDataLists;
struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

我们发现其实每个加锁的obj都对应一个SyncList,我们再看一下StripedMap的结构,

  • 我们发现StripedMap其实是一个抽象类,并且里面有一个indexForPointer的存储函数,
  • 而存储函数是通过哈希函数得到一个index,并且我们知道@synchronized(obj)可以全局使用,并且只要obj不为空都能进行加锁,
  • 所以我们猜测StripedMap应该有个全局的哈希表来通过obj得到一个index,然后存储SyncList,而SyncList存储的都是一个个的节点SyncData

我们再看一下id2data源码

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    
#if SUPPORT_DIRECT_THREAD_KEYS //环境定义--针对单个线程
    // Check per-thread single-entry fast cache for matching object

    bool fastCacheOccupied = NO;
    // 检查每线程单项快速缓存中是否有匹配的对象--
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {//
        fastCacheOccupied = YES;

        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            //取出线程单项快速缓存中加锁的次数
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {//加锁,则对加锁次数+1,然后重新缓存到线程的单项快速缓存中
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE://解锁,如果解锁,则对加锁次数-1,然后重新缓存到线程的单项快速缓存中
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {//lockCount为0时候就从tls中删除解锁
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE--记录的线程数去掉,threadCount是记录的线程的情况
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif//针对所有线程

    // Check per-thread cache of already-owned locks for matching object
    // 检查已拥有锁的每个线程高速缓存中是否有匹配的对象---跟上面操作类似
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int i;
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[i];
            if (item->data->object != object) continue;

            // Found a match.
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE:
                item->lockCount++;
                break;
            case RELEASE:
                item->lockCount--;
                if (item->lockCount == 0) {
                    // remove from per-thread cache
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }

    // Thread cache didn't find anything.
    // Walk in-use list looking for matching object
    // Spinlock prevents multiple threads from creating multiple 
    // locks for the same new object.
    // We could keep the nodes in some hash table if we find that there are
    // more than 20 or so distinct locks active, but we don't do that now.
    //加锁
    lockp->lock();

    {
        /*
         再次从缓存中取,看看有没有,避免有其他线程已经对obj加锁
         如果找到了就对threadCount+1
         如果找不到就在SyncData接一个data

         如果不是加锁咋不管
         如果是加锁,则将新的data的objc设置为object,并且将threadCount加1

         如果一个SyncData都没有那么就创建一个,设置好数据后保存在map中
         最后如果是新创建的,那么就会在tls中保存数据
         */
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {//再次从缓存中取一下
                result = p;
                // atomic because may collide with concurrent RELEASE
                //线程+1
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            //如果已经有了SyncData但是没有对应的object
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        //如果不是加锁则不管
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it
        if ( firstUnused != NULL ) {//则将object配置在后面的SyncData中
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

    // Allocate a new SyncData and add to list.
    // XXX allocating memory with a global lock held is bad practice,
    // might be worth releasing the lock, allocating, and searching again.
    // But since we never free these guys we won't be stuck in allocation very often.
    //如果一个SyncData都没有,那就创建一个
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) {//如果不是从缓存中读取的,那么就将SyncData缓存到tls中
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            // Save in thread cache
            if (!cache) cache = fetch_cache(YES);
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

    return result;
}

总结:

  • 1、当加锁时候,系统会从先从当前线程的tls中取出object对应的SyncData,如果取到了走到2,
    • 如果取不到就从全局SyncCache中遍历寻找与object对应的SyncCacheItem,如果取到了就走到2,
    • 如果取不到就走到3
  • 2、取到SyncData或者SyncCacheItem,先取出对象中的lockCount,代表的是加锁的次数,
    • 如果是加锁,则把lockCount加一,保存到线程的tls中,
    • 如果是解锁,则把lockCount减一,再保存到线程的tls中,减完之后如果lockCount为0时候,则将tls中的lockCount置空,并且通过OSAtomicDecrement32Barrier将SyncData的线程数置空,然后将SyncData或者SyncCacheItem返回
  • 3、假如都没找到,说明该对象以前没加锁过,为了保证不重复创建SyncData,下面这段是加锁状态,
    • 先从listp中遍历SyncData,如果找到与object对应的SyncData(可能其他线程创建了),那么直接跳到4;
    • 如果没找到object对应的SyncData,但是有空白的SyncData,则将空白的SyncData与object关联起来,并且将SyncData的threadCount设置为1,再跳到4;
    • 假如没有空白的SyncData,则创建一个SyncData,然后与object关联起来将SyncData的threadCount设置为1,并且将创建的SyncData变为listp的第一个元素(listp是通过object通过哈希算法得到的index存储在在map表中,因为不同的object可能得到相同的index,所以此时listp是已经有数据了,为了不让数据丢失,所以会把数据赋值在取到数据的nextData中,类似遵循栈特征,先进的在后面)
  • 4、准备好了SyncData,则解锁上面代码,如果是解锁,则不管,直接ruturn,如果是加锁,则将准备好的SyncData保存在当前线程tls的SYNC_DATA_DIRECT_KEY中,并将tls中的SYNC_COUNT_DIRECT_KEY设置为1,然后将SyncData返回;
  • 因为synchronized需要不断对map表已经缓存进行读写操作,所以性能比较低

做一个验证:

-(void)testSynchronized{

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (self) {
            NSLog(@"线程self---1加锁了");
            [self logInfo];
            sleep(3);
            NSLog(@"线程self---1解锁了");

        }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);//保证线程2加锁在线程1后面
        @synchronized (self) {
            NSLog(@"线程self---2加锁");
            [self logInfo];
            NSLog(@"线程self---2解锁");
        }
    });

    NSObject *objec = [NSObject new];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);//保证线程2加锁在线程1后面
        @synchronized (objec) {
            NSLog(@"线程objec---1加锁");
            [self logInfo];
            NSLog(@"线程objec---1解锁");
        }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);//保证线程2加锁在线程1后面
        @synchronized (objec) {
            NSLog(@"线程objec---2加锁了");
            [self logInfo];
            NSLog(@"线程objec2解锁了");
        }
    });
}

2020-03-05 21:09:49.299159+0800 003-NSLock分析[39805:6725003] 线程self---1加锁了
2020-03-05 21:09:49.299331+0800 003-NSLock分析[39805:6725003] 123
2020-03-05 21:09:50.301849+0800 003-NSLock分析[39805:6725002] 线程objec---1加锁
2020-03-05 21:09:50.302268+0800 003-NSLock分析[39805:6725002] 123
2020-03-05 21:09:50.302505+0800 003-NSLock分析[39805:6725002] 线程objec---1解锁
2020-03-05 21:09:50.302728+0800 003-NSLock分析[39805:6725011] 线程objec---2加锁了
2020-03-05 21:09:50.302925+0800 003-NSLock分析[39805:6725011] 123
2020-03-05 21:09:50.303114+0800 003-NSLock分析[39805:6725011] 线程objec2解锁了
2020-03-05 21:09:52.302250+0800 003-NSLock分析[39805:6725003] 线程self---1解锁了
2020-03-05 21:09:52.302565+0800 003-NSLock分析[39805:6725004] 线程self---2加锁
2020-03-05 21:09:52.302756+0800 003-NSLock分析[39805:6725004] 123
2020-03-05 21:09:52.302948+0800 003-NSLock分析[39805:6725004] 线程self---2解锁

我们发现对self和object加锁是互不影响的,只有对同一个对象加锁才有用,并且加锁的代码类似于加入了同步队列,依次执行

二、NSLock

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end

NSLock 遵循 NSLocking 协议,lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。

举个例子

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

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

2016-08-19 14:23:09.659 ThreadLockControlDemo[1754:129663] 线程1
2016-08-19 14:23:11.663 ThreadLockControlDemo[1754:129663] 线程1解锁成功
2016-08-19 14:23:11.665 ThreadLockControlDemo[1754:129659] 线程2

线程 1 中的 lock 锁上了,所以线程 2 中的 lock 加锁失败,阻塞线程 2,但 2 s 后线程 1 中的 lock 解锁,线程 2 就立即加锁成功,执行线程 2 中的后续代码。

-(void)testLock{
    NSLock *lock = [NSLock new];

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            sleep(2);
            NSLog(@"线程1");
            [lock unlock];
            NSLog(@"线程4");
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

            sleep(1);
            [lock lock];
            NSLog(@"线程5");
            [lock unlock];
            NSLog(@"线程6");
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        [lock tryLock];
        NSLog(@"线程2");
        [lock unlock];
        NSLog(@"线程3");
    });
}

2020-03-04 14:01:30.496134+0800 003-NSLock分析[37322:5093629] 线程2
2020-03-04 14:01:30.496493+0800 003-NSLock分析[37322:5093634] 线程5
2020-03-04 14:01:30.496490+0800 003-NSLock分析[37322:5093629] 线程3
2020-03-04 14:01:30.496758+0800 003-NSLock分析[37322:5093634] 线程6
2020-03-04 14:01:31.497772+0800 003-NSLock分析[37322:5093630] 线程1
2020-03-04 14:01:31.498132+0800 003-NSLock分析[37322:5093630] 线程4

由上面的结果可得知,当tryLock返回NO时候不会阻塞线程,意味着加锁失败,并且不管在哪个线程中执行unlock,那么其他线程堵塞也随之解除。

NSLock *lock = [[NSLock alloc] init];
    for (int i= 0; i<100; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
           
            static void (^testMethod)(int);
            
            testMethod = ^(int value){
                [lock lock];
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
                }
                [lock unlock];
            };
            testMethod(10);
        });
    }
    
    2020-03-04 15:16:51.971619+0800 003-NSLock分析[46466:5215601] current value = 10

我们会发现当出现这种循环递归锁时候,会造成堵塞现象,因为第一次testMethod执行时候lock进行加锁了,然后第二次进来时候lock需要等待解锁后才能往下走,而lock需要等第一个testMethod执行完才能解锁,这就造成了互相等待的阻塞现象

三、NSConditionLock

@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@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 NS_AVAILABLE(10_5, 2_0);

@end

NSConditionLock 和 NSLock 类似,都遵循 NSLocking 协议,方法都类似,只是多了一个 condition 属性,以及每个操作都多了一个关于 condition 属性的方法,例如 tryLock,tryLockWhenCondition:,NSConditionLock 可以称为条件锁,只有 condition 参数与初始化时候的 condition 相等,lock 才能正确进行加锁操作。而 unlockWithCondition: 并不是当 Condition 符合条件时才解锁,而是解锁之后,修改 Condition 的值,这个结论可以从下面的例子中得出。

    //主线程中
    NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];

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

    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);//以保证让线程2的代码后执行
        if ([lock tryLockWhenCondition:0]) {
            NSLog(@"线程2");
            [lock unlockWithCondition:2];
            NSLog(@"线程2解锁成功");
        } else {
            NSLog(@"线程2尝试加锁失败");
        }
    });

    //线程3
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(2);//以保证让线程2的代码后执行
        if ([lock tryLockWhenCondition:2]) {
            NSLog(@"线程3");
            [lock unlock];
            NSLog(@"线程3解锁成功");
        } else {
            NSLog(@"线程3尝试加锁失败");
        }
    });

    //线程4
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(3);//以保证让线程2的代码后执行
        if ([lock tryLockWhenCondition:2]) {
            NSLog(@"线程4");
            [lock unlockWithCondition:1];    
            NSLog(@"线程4解锁成功");
        } else {
            NSLog(@"线程4尝试加锁失败");
        }
    });

2016-08-19 13:51:15.353 ThreadLockControlDemo[1614:110697] 线程2
2016-08-19 13:51:15.354 ThreadLockControlDemo[1614:110697] 线程2解锁成功
2016-08-19 13:51:16.353 ThreadLockControlDemo[1614:110689] 线程3
2016-08-19 13:51:16.353 ThreadLockControlDemo[1614:110689] 线程3解锁成功
2016-08-19 13:51:17.354 ThreadLockControlDemo[1614:110884] 线程4
2016-08-19 13:51:17.355 ThreadLockControlDemo[1614:110884] 线程4解锁成功
2016-08-19 13:51:17.355 ThreadLockControlDemo[1614:110884] 线程1

上面代码先输出了 ”线程 2“,因为线程 1 的加锁条件不满足,初始化时候的 condition 参数为 0,而加锁条件是 condition 为 1,所以加锁失败。locakWhenCondition 与 lock 方法类似,加锁失败会阻塞线程,所以线程 1 会被阻塞着,而 tryLockWhenCondition 方法就算条件不满足,也会返回 NO,不会阻塞当前线程。

回到上面的代码,线程 2 执行了 [lock unlockWithCondition:2]; 所以 Condition 被修改成了 2。

而线程 3 的加锁条件是 Condition 为 2, 所以线程 3 才能加锁成功,线程 3 执行了 [lock unlock]; 解锁成功且不改变 Condition 值。

线程 4 的条件也是 2,所以也加锁成功,解锁时将 Condition 改成 1。这个时候线程 1 终于可以加锁成功,解除了阻塞。

从上面可以得出,NSConditionLock 还可以实现任务之间的依赖。

四、NSRecursiveLock

@interface NSRecursiveLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end

NSRecursiveLock 是递归锁,他和 NSLock 的区别在于,NSRecursiveLock 可以在一个线程中重复加锁(反正单线程内任务是按顺序执行的,不会出现资源竞争问题),NSRecursiveLock 会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。

像上面NSLock时候在一个线程中重复加锁的话,会造成堵塞现象,我们可以吧NSLock改成NSRecursiveLock就可以了

    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        static void (^RecursiveBlock)(int);
        RecursiveBlock = ^(int value) {
            [lock lock];
            if (value > 0) {
                NSLog(@"value:%d", value);
                RecursiveBlock(value - 1);
            }
            [lock unlock];
        };
        RecursiveBlock(2);
    });

2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:2
2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:1

五、NSCondition

@interface NSCondition : NSObject <NSLocking> {
@private
    void *_priv;
}

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end

NSCondition 的对象实际上作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程,即线程是否要进入 waiting 状态,经测试,NSCondition 并不会像上文的那些锁一样,先轮询,而是直接进入 waiting 状态,当其它线程中的该锁执行 signal 或者 broadcast 方法时,线程被唤醒,继续运行之后的方法。

-(void)testCondition{

    NSCondition *lock = [[NSCondition alloc] init];
    NSMutableArray *array = [[NSMutableArray alloc] init];
    //线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            [lock lock];
            if (!array.count) {
                [lock wait];
            }
            [array removeAllObjects];
            NSLog(@"array removeAllObjects");
            [lock unlock];
    });
    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);//以保证让线程2的代码后执行
        [lock lock];
        [array addObject:@1];
        NSLog(@"array addObject:@1");
        [lock signal];
        NSLog(@"发送信号");
        [lock unlock];
    });
}

2020-03-04 18:30:07.484889+0800 003-NSLock分析[54265:5454159] array addObject:@1
2020-03-04 18:30:07.485264+0800 003-NSLock分析[54265:5454159] 发送信号
2020-03-04 18:30:07.485530+0800 003-NSLock分析[54265:5454160] array removeAllObjects

NSCondition锁可以对多个线程同时进行加锁,并且在一个线程加锁后可以通过wait让线程暂停下来,只有其他线程发送了signal或者broadcast信号才能继续往下执行,其中 signal 和 broadcast 方法的区别在于,signal 只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用,而 broadcast 可以唤醒所有在等待的线程。如果没有等待的线程,这两个方法都没有作用。

六、锁的分类

1、自旋锁 - atomic

线程反复检查锁变量是否可用,由于线程在这一过程中保持执行,因此是一种忙等待,一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁,自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的,而atomic就是通过个set和get方法添加一个自旋锁,关于atomic和nonatomic看这篇文章atomic和nonatomic的分析

2、互斥锁

互斥锁是用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制,该目的通过将代码切成一个个的临界区而达成,就像假如一个线程在执行还没解锁,那当前线程就休息一会,自旋锁是盲等,意思就是这个线程没完成,那另一个线程只能在那等待,所以自旋转比互斥锁性能消耗高,但是效率是最高的

属于互斥锁的有

  • NSLock
  • pthread_mutex
  • @synchronized

3、读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行访问,写者则需要对共享资源进行写操作,

这种锁对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同事多个读者俩访问共享资源,最大可能的读者数为实际的逻辑CPU数,写者是排他性的,一个读写锁同事只能有一个写者或多个读者,但不能同时既有读者又有写者,在读写锁保持期间也是抢占失效的。

如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,知道没有任何写者或读者,如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在哪里,知道写者释放该读写锁

一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁,正式因为这个特性

  • 当读写锁是写加锁状态时,在这个锁呗解锁之前,所有视图对这个锁加锁的线程都会被堵塞
  • 当对俄所在读加锁状态时,所有视图以读模式对她进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对次锁,它必须直到所有的线程释放锁
  • 通常,当读写锁处于读模式锁住状态时,如果有另外线程试图以写模式加锁,读写锁通常会阻塞随后的读模式请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求长期阻塞
  • 读写锁适合对数据结构的读次数比写次数多得多的情况,因为,读模式锁定时可以共享,以写模式锁住时一位置独占,所以读写锁又叫共享-独占锁

实现:

#pragma mark - 读数据
- (id)lg_objectForKey:(NSString *)key{
    __block id obj;
    // 同步读取指定数据:
    dispatch_sync(self.concurrent_queue, ^{
        obj = [self.dataCenterDic objectForKey:key];
    });
    return obj;
}

#pragma mark - 写数据
- (void)lg_setObject:(id)obj forKey:(NSString *)key{
    // 异步栅栏调用设置数据:
    dispatch_barrier_async(self.concurrent_queue, ^{
        [self.dataCenterDic setObject:obj forKey:key];
    });
}

我们可以创建一个并发队列,对于读操作,我们可以用同步函数进行处理,对于写操作我们可以用异步栅栏函数这操作,这样就简单实现了一个读写锁