iOS 锁

166 阅读5分钟

前言

锁是为了解决多线程的安全问题。 如下实例: image.png

count在线程中顺序输出10次,但是并没有顺序输出;

线程安全

多线程操作共享数据的时候,不会出现意想不到的结果,就是线程安全,否则就是线程不安全。

原子属性能否保证线程安全?

查看get和set的部分源码: image.png image.png 原⼦属性只能保障set或者get的读写安全,但我们在使⽤属性的时候,往往既有set⼜有get,所以说原⼦属性并不是线程安全的。

自旋锁和互斥锁

  • ⾃旋锁: 在访问被锁的资源的时候,调⽤者线程不会休眠,⽽是不停循环在那⾥,直到被锁资源释放锁。(忙等)
  • 互斥锁: 在访问被锁资源时,调⽤者线程会休眠,此时cpu可以调度其他线程⼯作。直到被锁的资源释放锁。然后再唤醒休眠线程。(闲等)

自旋锁优缺点

  • ⾃旋锁的优点在于,因为⾃旋锁不会引起调⽤者线程休眠,所以不会进⾏线程调度,cpu时间⽚轮转等⼀些耗时的操作。所以如果能在很短的时间内获得锁,⾃旋锁的效率远⾼于互斥锁。
  • ⾃旋锁缺点在于,⾃旋锁⼀直占⽤CPU,在未获得锁的情况下,⼀直⾃旋,相当于死循环,会⼀直占⽤着CPU,如果不能在很短的时间内获得锁,这⽆疑会使CPU效率降低。 ⽽且⾃旋锁不能实现递归调⽤。

⾃旋锁优先级反转的bug

当多个线程有优先级的时候,如果⼀个优先级低的线程先去访问某个数据,此时使⽤⾃旋锁进⾏了加锁,然后⼀个优先级⾼的线程⼜去访问这个数据,那么优先级⾼的线程因为优先级⾼会⼀直占着CPU资源,此时优先级低的线程⽆法与优先级⾼的线程争夺 CPU 时间,从⽽导致任务迟迟完不成、锁⽆法释放。

OSSpinLock

  • 是一种自旋锁,iOS10之后被移除。
  • OS_SPINLOCK_INIT 初始化锁 。
  • OSSpinLockLock(&spinlock) 加锁,参数为OSSPINLOCK地址。
  • OSSpinLockUnlock(&spinlock) 解锁,参数是OSSpinLock地址。
  • OSSpinLockTry(&spinlock) 尝试上锁,参数是OSSpinLock地址。如果返回false,表示上锁失败,锁正在被其他线程持有。如果返回true,表示上锁成功。
  • 由于⾃旋锁本身存在的这个问题,所以苹果在iOS10以后已经废弃了OSSpinLock。
  • 也就是说除⾮⼤家能保证访问锁的线程全部都处于同⼀优先级,否则 iOS 系统中的⾃旋锁就不要去使⽤了。

os_unfair_lock

  • iOS10之后开始支持,用于取代OSSpinLock。
  • OS_UNFAIR_LOCK_INIT 初始化锁。
  • os_unfair_lock_lock 加锁。参数为os_unfair_lock地址。
  • os_unfair_lock_unlock 解锁。参数为os_unfair_lock地址。
  • os_unfair_lock_trylock 尝试加锁。参数为os_unfair_lock地址。如果成功返回true。如果锁已经被锁定则返回false。
  • os_unfair_lock_assert_owner 参数为os_unfair_lock地址。如果当前线程未持有指定的锁或者锁已经被解锁,则触发崩溃。
  • os_unfair_lock_assert_not_owner 参数为os_unfair_lock地址。如果当前线程持有指定的锁,则触发崩溃
self.unfailLock = OS_UNFAIR_LOCK_INIT;
- (void)testOsUnfairLock{
    os_unfair_lock_lock(&_unfailLock);
    self.count --;
    NSLog(@"%ld",self.count);
    os_unfair_lock_unlock(&_unfailLock);
}

NSLock

  • -(void)lock 加锁。
  • -(void)unlock 解锁。
  • -(BOOL)tryLock 尝试加锁。成功返回YES,失败返回NO。
  • -(BOOL)lockBeforeDete:(NSDate *)limit 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO。
  • @property (nullable ,copy) NSString *name 锁名称。
self.lock = [[NSLock alloc] init];
self.lock.name = @"name";
- (void)test_lock{
    [self.lock lock];
    self.count --;
    NSLog(@"%ld",self.count);
    [self.lock unlock];
}

NSRecursiveLock

  • -(void)lock 加锁。
  • -(void)unlock 解锁。
  • -(BOOL)tryLock 尝试加锁。成功返回YES,失败返回NO。
  • -(BOOL)lockBeforeDete:(NSDate *)limit 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO。
  • @property (nullable ,copy) NSString *name 锁名称。
-(void)test_recursiveLock {
    [self.recursiveLock lock];
    self.count --;
    NSLog(@"%ld",self.count);
    [self.recursiveLock unlock];
}

NSRecursiveLock 是递归锁,可以递归调用,但是只能在同一个线程递归调用,不能被多条线程所拥有;

NSCondition

  • -(void)lock 加锁。
  • -(void)unlock 解锁。
  • -(void)wait 阻塞当前线程,使线程进入休眠,等待唤醒信号。调用前必须已加锁。
  • -(void)waitUntilDate 阻塞当前线程,使线程进入休眠,等待唤醒信号或者超时。调用前必须已加锁。
  • -(void)signal 唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁。
  • -(void)broadcast 唤醒所有在等待的线程。如果没有线程在等待,则什么也不做。调用前必须已加锁。
  • @property (nullable ,copy) NSString *name 锁名称。

生产者消费者实例

- (void)test_nscondition{
    //生产
    for (NSInteger i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self test_production];
        });
    }
    for (NSInteger i = 0; i < 100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self test_consumption];
        });
    }
}

- (void)test_production {
    [self.condition lock];
    self.count ++;
    NSLog(@"生产了一个产品,现有产品 : %ld个",(long)self.count);
    [self.condition signal];
    [self.condition unlock];
}

- (void)test_consumption {
    [self.condition lock];
    //这里如果改成if会因为虚假唤醒,出现负数
    while ( self.count == 0 ) {
        [self.condition wait];
    }
    self.count --;
    NSLog(@"消费了一个产品,现有产品: %ld个",(long)self.count);
    [self.condition unlock];
}

NSCondition存在虚假唤醒

  • 当线程从等待已发出信号的条件变量中醒来,却发现它等待的条件不满⾜时,就会发⽣虚假唤醒。之所以称为虚假,是因为该线程似乎⽆缘⽆故地被唤醒了。但是虚假唤醒不会⽆缘⽆故发⽣:它们通常是因为在发出条件变量信号和等待线程最终运⾏之间,另⼀个线程运⾏并更改了条件。线程之间存在竞争条件,典型的结果是有时,在条件变量上唤醒的线程⾸先运⾏,赢得竞争,有时它运⾏第⼆,失去竞争。
  • 在许多系统上,尤其是多处理器系统上,虚假唤醒的问题更加严重,因为如果有多个线程在条件变量发出信号时等待它,系统可能会决定将它们全部唤醒,将每个signal( )唤醒⼀个线程视为broadcast( )唤醒所有这些,从⽽打破了信号和唤醒之间任何可能预期的 1:1 关系。如果有 10 个线程在等待,那么只有⼀个会获胜,另外 9 个会经历虚假唤醒。

NSConditionLock

  • 基于NSCondition
  • -(void)lock 加锁。
  • -(void)unlock 解锁。
  • -(instancetype)initWithCondition:(NSinteger)初始化一个。NSConditionLock对象。
  • @property(readonly) NSInteger condition 锁的条件。
  • -(void)lockWhenCondition:(NSInteger)conditio满足条件时加锁。
  • -(BOOL)tryLock尝试加锁。
  • -(BOOL)tryLockWhenCondition如果接受对象的condition与给定的condition相等,则尝试获取锁,不足塞线程。
  • -(void)unlockWithCondition:(NSInteger)condition解锁,重置锁的条件。
  • -(BOOL)lockBeforDate:(NSDate *)limit在指定时间点之前获取锁。
  • -(BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit在指定的时间前获取锁。
  • @property (nullable ,copy) NSString *name 锁名称。 可以用来控制调用线程顺序
- (void)lg_testConditonLock{
    self.iConditionLock = [[NSConditionLock alloc] initWithCondition:3];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.iConditionLock lockWhenCondition:3];
        NSLog(@"线程 1");
        [self.iConditionLock unlockWithCondition:2];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.iConditionLock lockWhenCondition:2];
        NSLog(@"线程 2");
        [self.iConditionLock unlockWithCondition:1];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.iConditionLock lockWhenCondition:1];
        NSLog(@"线程 3");
        [self.iConditionLock unlockWithCondition:0];
    });
}

pthread_mutex

  • pthread_mutex_init(pthread_mutex_t mutex,const pthread_mutexattr_t attr)初始化锁,pthread_mutexattr_t可用来设置锁的类型。
  • pthread_mutex_lock(pthread_mutex_t mutex);加锁
  • pthread_mutex_trylock(*pthread_mutex_t *mutex);加锁,但是上面方法不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待,成功返回0.失败返回错误信息
  • pthread_mutex_unlock(pthread_mutex_t *mutex);释放锁
  • pthread_mutex_destroy(pthread_mutex_t* mutex);使用完锁之后释放锁
  • pthread_mutexattr_setpshared();设置互斥锁的范围
  • pthread_mutexattr_getpshared();获取互斥锁的范围
- (void)test_pthread_mutex {
    //非递归
    pthread_mutex_t lock0;
    pthread_mutex_init(&lock0, NULL);
    pthread_mutex_lock(&lock0);
    //要锁的内容
    pthread_mutex_unlock(&lock0);
    //C语言封装,使用完后要手动释放
    pthread_mutex_destroy(&lock0);
    
    //递归
    pthread_mutex_t lock;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&lock, &attr);
    pthread_mutexattr_destroy(&attr);
    pthread_mutex_lock(&lock);
    //要锁的内容
    pthread_mutex_unlock(&lock);
    pthread_mutex_destroy(&lock);
}

dispatch_semaphore_t

  • dispatch_semaphore_create(long value)这个函数是创建⼀个dispatch_semaphore_t类型的信号量,并且创建的时候需要指定信号量的⼤⼩。
  • dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)等待信号量。如果信号量值为0,那么该函数就会⼀直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值⼤于等于1,该函数会对信号量的值进⾏减1操作,然后返回。
  • dispatch_semaphore_signal(dispatch_semaphore_t deem)发送信号量。该函数会对信号量的值进⾏加1操作。
- (void)test_dispatch_semaphore_t {
    
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务1");
        dispatch_semaphore_signal(sem);
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务2");
        dispatch_semaphore_signal(sem);
    });
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务3");
    });
}

读写锁

GCD实现读写锁,多读单写,读写互斥,dispatch_barrier_async和并行队列实现

//MARK:-读写锁,多读单写,读写互斥
 self.queue = dispatch_queue_create("yl", DISPATCH_QUEUE_CONCURRENT);
 self.dataDic = [NSMutableDictionary dictionary];
- (void)test_read {
    for (NSInteger i = 0; i<10; ++i ) {
        dispatch_async(self.queue, ^{
            [self readNow:@"name"];
        });
    }
}
-(NSString *)readNow:(NSString *)key {
    __block NSString *readName;
    //异步读取
    dispatch_sync(self.queue, ^{
        readName = self.dataDic[key];
    });
    return readName;
}
- (void)test_write:(NSDictionary *)dict{
    dispatch_barrier_async(self.queue, ^{
        [self.dataDic setDictionary:dict];
    });
}

@synchronized

可以在多个线程下递归调用

- (void)test_synchronized{
    @synchronized (self) {
        self.count --;
        NSLog(@"%ld",self.count);
    }  
}

使用clang编译,后可以看出,@synchronized的加锁和解锁是objc_sync_enterobjc_sync_exit两个函数相关,查看objc源码,

objc_sync_enter Xnip2022-06-02_09-34-34.jpg objc_sync_exit Xnip2022-06-02_09-35-07.jpg

  • 当传入的参数是空的时候,都不做处理,不会加锁和解锁;
  • 当加锁和解锁的时候都和SyncData相关
  • 都调用ida2Data方法

SyncData的数据结构

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData; //单向链表指针,到下一个节点
    DisguisedPtr<objc_object> object; //传入参数的封装
    int32_t threadCount;  // number of THREADS using this block(记录使用block的数量)
    recursive_mutex_t mutex;//递归锁
} SyncData;

ida2Data函数

  • TLS(Thread Local Storage)就是线程局部存储,是操作系统为线程单独提供的私有空间,能存储只属于当前线程的⼀些数据。
  1. 从线程的TLS快速缓存中找
  2. 从线程的中TLS的缓存中找
  3. 如果从线程缓存中没有找到,从StripedMap表中找,如果找到加的TLS缓存中;
  4. 如果表中没有找到,加锁,并且放入从StripedMap表中;

总结

@synchronize针对某个对象,也就是我们给@synchronize传的参数,每⼀条线程都有⼀把递归锁,⽽且记录了每条线程加锁的次数,这样就能通过这俩点,对每条线程⽤不同的递归锁来进⾏加锁和解锁的的操作,从⽽达到多线程递归调⽤的⽬的。