OC 底层原理(18)- 锁

1,103 阅读12分钟

@synchronized

作用:线程安全

基本使用

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

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

}

打印结果:

为了加了 synchronized 能保证线程安全,为什么呢?

在汇编环境有看到在执行 synchronized 力里有两个方法 objc_sync_enter,objc_sync_exit

锁的源码分析

源码获取地址:

此段代码便是研究 synchronized 的入口,注释有提到 synchronized 是一种递归锁,并且递归锁是一种特殊的互斥锁,互斥锁,分为递归和非递归

  • 互斥锁(mutex):用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败是,线程会进入睡眠,等待锁释放时被唤醒。

  • 如果对象存在变获取到 SyncData

SyncData 结构体是一种链式结构,存储了:结点、当前对象、当前的线程数,关联的锁

  • 如果对象 objc 不存在,就 objc_sync_nil,什么事情都不做,也就是不加锁
BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);

  • 递归锁与 objc_sync_nil 搭配使用,防止死锁

  • SyncData* data = id2data(obj, ACQUIRE);

  • 也就是有一个全局的map,是一个个 SynList 表结构,里面有一个个结点 syndata(obj)

  • static SyncData* id2data(id object, enum usage why) 源码

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    
    // @syn - vc model view 任何地方都可以直接使用 - 全局
    // data 存储节点
    // map - obj - 表
    
#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: {
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    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();

    {
        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
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            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 ) {
            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.
    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) {
            // 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;
}

static SyncData* id2data(id object, enum usage why) 对互斥锁的一种形式的封装,是一种相应的递归封装,里面是表结构,利用哈希进行存储

代码分析

  • 这段代码,主要是在加锁之前回去用当前的对象去每个线程的哈希表里去查找是否 data 已经存在
  • 如果已经存,
    • 获取到 lockCount 大于 1 ,说明一出现重入,被锁多次,也就是递归锁;
    • 当 why表示标识为 ACQUIRE 时,lockCount 加 1,如果为 RELEASE 时,lockCount 减 1,如果 lockCount 为 0 时,便去掉当前 threadCount
    • 但是不会新建结点存入 sDataLists 锁列表里,
  • 如果不存在,就会去锁的缓存里去找,如果还是找不到的话;
  • 就会去其他线程里去查找,如果找到了 threadCount 就递增。
  • 要是再整个 list 都找不到就创建一个结点,加入 sDataLists。
  • lockCount 记录重入,可以被锁对此,是否接入递归
  • threadCount 继续线程情况,是否接入死锁

NSLock

- (void)lg_crash{
    
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            _testArray = [NSMutableArray array];

        });
    }
}

运行结果

打印结果分析

  • 这个是一个很大循环里使用异步函数初始化数组
  • 由于是异步函数,所以没循环一次就会产生一个新线程,所以,最红会产生很多很多的线程同时在执行初始化数组。
  • 由于所有的对象都是由 self 持有,所以都是同一个地址
  • 在初始化一个新数组对象时,旧的对象也会释放 release
  • 因为同时有多个线程在对此地址操作,所以在释放的过程就可能发生对已对方了的对象再次进行释放操作,也就是过度释放,产生野指针,就会崩溃
  • 可以加个锁来解决这个问题

加锁 synchronized

- (void)lg_crash{
    for (int i = 0; i < 200000; i++) {
        @synchronized (_testArray) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                    _testArray = [NSMutableArray array];
                });
            }
        }
}

运行结果:

此时还是会崩溃,原因:在第一次加锁的时候,去哈利表里查找对象objc,如果找不到的话就会创建一个结点,要是能找到的话,就不会创建了,所以这里加的锁,除了第一个,后面都是空锁,也就是相当于就是没有加。所以最后就会有问题。

换一个锁 NSLock

- (void)lg_crash{
    NSLock *lock = [[NSLock alloc] init];
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            _testArray = [NSMutableArray array];
            [lock unlock];

        });
    }
}

这样优化就不会崩溃了

  • 经过这两个锁的比较,在当前情况下选用 NSlock 会好些

Lock 案例

- (void)lg_testRecursive{
  
    NSLock *lock = [[NSLock alloc] init];

    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);
    });
}
  • 此时会发生堵塞,因为进入 testMethod 时,便会锁一次;
  • 然后还没等到解锁里面调用了testMethod(value - 1);接着就会又被锁一次,
  • 所以这里就不能随便用一个互斥锁了
  • 在递归环境下就要换着用个递归锁

代码优化,使用递归锁 NSRecursiveLock

- (void)lg_testRecursive{
  
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

    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);
    });
}
  • 死锁造成的原因,就是没有找到合适的出口

案例

- (void)lg_testRecursive{
    for (int i= 0; i<100; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
           
            static void (^testMethod)(int);
            
            testMethod = ^(int value){
                
                @synchronized (self) {
                    if (value > 0) {
                      NSLog(@"current value = %d",value);
                      testMethod(value - 1);
                    }
                }
            };
            testMethod(10);
        });
    } 
}

总结

  • 普通线程安全 --- NSLock
  • 递归调用 ----- NSRecursiveLock
  • 在递归调用的同时,还有循环并且还有外界线程影响的时候,更多的关注死锁---- synchronized

条件锁汇编分析

NSCondition

NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件觉得是否继续运行线程,即线程是否阻塞。

  • [condition lock]; 一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在 lock 外等待,只要 unlock, 才可访问。
  • [condition unlock]; 与lock 同时使用
  • [condition wait];让当前线程处于等待状态
  • [condition signal];CPU 发现号告诉线程不用再等待,可以继续执行

**生产者与消费者 **

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@property (nonatomic, strong) NSCondition *testCondition;

@end

@implementation ViewController

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



#pragma mark -- NSCondition

- (void)lg_testConditon{
    
    _testCondition = [[NSCondition alloc] init];
    //创建生产-消费者
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
        
    }
}

- (void)lg_producer{
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);

}

- (void)lg_consumer{
    
    while (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
    }
    
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
}
@end

打印结果:

从打印看出,结果并不正常,线程并不安全

代码优化

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@property (nonatomic, strong) NSCondition *testCondition;

@end

@implementation ViewController

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



#pragma mark -- NSCondition

- (void)lg_testConditon{
    
    _testCondition = [[NSCondition alloc] init];
    //创建生产-消费者
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
        
    }
}

- (void)lg_producer{
    [_testCondition lock];
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_testCondition signal];
    [_testCondition unlock];

}

- (void)lg_consumer{
    
    // 线程安全
    [_testCondition lock];

    while (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        // 保证正常流程
        [_testCondition wait];
    }
    
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
    [_testCondition unlock];
}

打印结果:

现在打印就正常了

NSConditionLock

  • NSConditionLock 是锁,一旦一个线程获得锁,其他线程一定等待
  • [xxx lock];表示 xxx 期待获得锁,如果没有其他线程获得锁,那他能执行此行一下代码,如果已经有其他线层获得锁(可能是条件锁,或者无条件锁),则等待,直至其他线程解锁。
  • [xxx lockWhenCondition:A 条件];表示如果没有其他线程获得该锁,但是该锁内部的 condition 不等于 A 条件,它依然不能获得锁,仍然等待。如果内部的 condition 等于 A 条件,并且没有其他线程获得该锁,则进入代码区,同时设置他获得该锁,其他任何线程都将等待它代码的完成,直至它解锁。
  • [xxx unlockWhenCondition:A 条件]; 表示释放锁,同时把内部的 condition 设置为 A 条件
  • return = [xxx lockWhenCondition:A 条件 beforeDate:A 时间];表示如果被锁定(没获得锁),并超过该时间则不再阻塞线程,但是注意:返回值是 NO,它没有改变锁的状态,这个函数的目的在于可以实现两种状态下的处理。
  • 所谓的 condition 就是整数,内部通过整数比较条件

案例

#pragma mark -- NSConditionLock
- (void)lg_testConditonLock{
    // 信号量
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
       [conditionLock lockWhenCondition:1];
       NSLog(@"线程 1");
       [conditionLock unlockWithCondition:0];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       
       [conditionLock lockWhenCondition:2];
       
       NSLog(@"线程 2");
       
       [conditionLock unlockWithCondition:1];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
       [conditionLock lock];
       NSLog(@"线程 3");
       [conditionLock unlock];
    });
}

打印结果

打印总结

  • 线程 1 调用 [conditionLock lockWhenCondition:1];此时此刻因为不满足当前条件,所以会进入 waiting 状态,当前进入到 waiting 时,会释放当前的互斥锁。
  • 此时当前的线程 3 调用 [conditionLock lock]; 本质上是调用 [conditionLock lockBeforeDate:];,这里不需要比对条件值,所以线程 3 会打印
  • 接下来线程 2 执行 [conditionLock lockWhenCondition:2];,因为满足条件值,所以线程 2 会打印,打印完成后会调用 [conditionLock unlockWithCondition:1];这个时候讲 value 设置为 1,并发送 boradcast,此时线程 1 接收到当前的信号,唤醒执行并打印。
  • 自此当前答应为:线程3 -> 线程2 -> 线程1.
  • [conditionLock lockWhenCondition:];这里会根据传入的condition 值和 value 值进行对比,如果不相等,这里就会阻塞,进入线程池,否则的话就继续代码执行
  • [conditionLock unlockWithCondition:];这里会先更改当前的value 值,然后进行广播 boradcast,唤醒当前的线程。

属性修饰 nonatomic 和 natomic 的区别

图片所展示的,nonatomic 和 natomic 区别就是

  • nonatomic 在赋值时,就直接替换旧值赋值
  • natomic 在赋值时用了一个自旋锁

锁的归类

  • 自旋锁:线程反复检查锁变量是否可用,由于线程在这一过程中保存执行,因为是一种盲等待,。一旦获取了自旋锁,线程会一直保存该锁,直至显示释放自旋锁。自旋锁避免了进程上下文额调度开销,因为对于线程只会阻塞很短时间的场合是有效的。
    • natomic:自旋锁锁住该属性,因此它会消耗更多的资源,性能会很低。要比nonatomic 慢 20 倍。
    • spinlock:性能问题-弃用-iOS10
  • 互斥锁:是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制,该目的通过将代码片切成一个一个的临界区而达成。属于互斥锁的有:
    • NSLock
    • pthread_mutex
    • @synchronized
    • os_unfair_lock
  • 条件锁: 就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了,当资源被分配到了,条件锁就打开,进行继续运行
    • NSCondition
    • NSConditionLock
  • 递归锁:就是同一个线程可以加锁 N 次而不会引发死锁
    • NSRecursiveLock
    • pthread_mutex(recursive)
  • 信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是semaphore 在仅取值 0/1 时的特别,信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
    • dispatch_semaphore
  • 读写锁:是一种特殊的自旋锁,它把对共享资源的访问者分为读者和写者,读者只对共享资源进行读访问,写着则需要对共享资源进行写操作,这种锁相对于自旋锁而言,能提高并发性,因为在多处理系统中,它允许同时有多个读者来访问共享资源,写者是排他性,一个读写锁同时只能有一个写者多个读者(与CPU数相关),但不能同时既有读者又有写者,在读写锁保存期间也是抢占失效的。
  • 其实基本的锁就包括了三类:自旋锁、互斥锁、读写锁
  • 其他的不如添加锁、递归锁、信号量都是上层的封装和实现