11 - OC多线程之锁的认识

OC底层原理探索文档汇总

主要内容:

1、常见锁
2、锁的使用场景

1、常见锁的认识

1.1 什么是锁,为什么需要锁

在多线程编程中,为了防止多个线程对同一个资源进行读写操作而导致的数据不安全问题需要使用锁来实现,比如我在线程一进行获取操作,在线程二进行赋值操作。因线程抢占CPU的不确定性,有可能在赋值前进行获取操作。

因此我们需要让同一代码一次只能被一个线程执行,这就需要锁来实现。
锁的作用就是将会造成线程安全问题的代码块让多线程一个执行完再让下一个线程执行。

1.2 我们可以用哪些锁

总的来说可以分为两大类,互斥锁和自旋锁

互斥锁包括@synchronized、NSLock、递归锁、条件锁、信号量、读写锁
自旋锁包括OSSpinLock和os_unfair_lock,其中OSSpinLock已经废弃掉了,因此本文只分析os_unfair_lock。

锁性能比较.png

1.3 互斥锁

互斥锁是当有其他线程进行处理时,便会处于等待(阻塞)状态,一直等到被唤醒就处于就绪状态,等待CPU调度就可以执行了。

1.3.1 @synchronized

使用:

- (void)synchronizedTest{
    for (int i=0; i<200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (self) {
                self.person = [[WYPerson alloc] init];
                NSLog(@"wenyi-%d",i);
            }
        });
        
    }
}

代码中的person赋值,会不停的进行retain和release,如果在retain和release不匹配,就会造成崩溃,所以就需要通过@synchronized来加锁实现。

注意

  • 通过查看源码可知@synchorized是一把递归互斥锁,可重入,未释放可再次加锁
  • 可重入的表现为同一个线程可以重复锁;多个线程可以重复加锁
  • 通过链表结构实现了递归的特点,每个线程的缓存中均对锁对象进行存储,通过lockCount、threadCount的记录可以判断递归的次数。
  • 锁并不一定是self,要知道锁的生命周期与锁住的内容的生命周期,合理选择锁。最好生命周期相同
  • 由于底层中链表查询、缓存的查找以及递归,是非常耗内存以及性能的,导致性能低,所以在前文中,该锁的排名在最后
  • 但是目前该锁的使用频率仍然很高,主要是因为方便简单,且不用解锁
  • 不能使用非OC对象作为加锁对象,因为其object的参数应当为id

1.3.2 NSLock

- (void)lockTest{
    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);
                }
            };
            testMethod(10);
            [lock unlock];
        });
    }
}

说明:

  • 使用起来很简单,就是通过lock和unlock进行加锁减锁,在其内部的代码就是线程安全的代码
  • 它只是单纯的互斥锁,不是递归锁,所以上面代码会出现一直等待的现象
  • 遵循了NSLocking协议,底层通过pthread_mutex实现的。
  • 从上文的性能图中可以看出NSLock的性能仅次于 pthread_mutex(互斥锁),非常接近

1.3.3 pthread_mutex

pthread_mutex就是互斥锁本身,当锁被占用,其他线程申请锁时,不会一直忙等待,而是阻塞线程并睡眠。

// 导入头文件
#import 

// 全局声明互斥锁
pthread_mutex_t _lock;

// 初始化互斥锁
pthread_mutex_init(&_lock, NULL);

// 加锁
pthread_mutex_lock(&_lock);
// 这里做需要线程安全操作
// 解锁 
pthread_mutex_unlock(&_lock);

// 释放锁
pthread_mutex_destroy(&_lock);

注:基本用不到,仅做了解

1.4 递归锁NSRecursiveLock

递归锁也是一种互斥锁,但是它用在递归函数的线程安全中,不解锁可以再次加锁,带有递归性质的互斥锁

使用上和NSLock一样,它只比NSLock多了一个就是可以用在递归函数中。

- (void)recursiveLockTest{
    NSRecursiveLock *lock = [[NSRecursiveLock 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);
                }
            };
            testMethod(10);
            [lock unlock];
        });
    }

}

为什么互斥锁会在递归函数中死锁?

  • 因为加锁之后还没有解锁就进入下一个循环再去加锁,就会死锁了。
  • 死锁是因为线程会等待锁的释放而进行休眠状态,但是这个锁又不可能被释放,就会一直处于等待状态,无法执行下去,一直处于死锁状态。

注意:

  • NSRecursiveLock在底层也是对pthread_mutex的封装
  • 区别仅在于多了一层递归逻辑

1.5 自旋锁

自旋锁也可以实现在任一时刻只能有一个线程去执行代码。

自旋锁与互斥锁的区别在于: 互斥锁如果发现资源已经被占用了,也就是发现已经被上锁了,就会进入睡眠状态,但自旋锁不会睡眠,而是以忙等待的状态一直不停的查看是否已经被释放了。

重点在于忙等待,不停的进行查看。

自旋锁避免了线程上下文调度开销,因此对于线程只会阻塞很短时间的场合是有效的,所以性能是很高的。

自从OSSpinLock出现安全问题,在iOS10之后就被废弃了。在OSSpinLock被弃用后,其替代方案是内部封装了os_unfair_lock,而os_unfair_lock在加锁时会处于休眠状态,而不是自旋锁的忙等状态

    os_unfair_lock_t unfairLock;
    unfairLock = &(OS_UNFAIR_LOCK_INIT);
    os_unfair_lock_lock(unfairLock);
    os_unfair_lock_unlock(unfairLock);

注意:

  • atomic自带一把自旋锁。内部就是通过os_unfair_lock来实现的

自旋锁为什么不安全呢?
优先级反转。低优先级的线程先拿到锁,高优先级的线程就会进入忙等状态,也就是会一直循环的查看锁是否被释放,占用了CPU的调度,导致低优先级的无法被CPU调度到,也就无法完成任务释放锁。

有两种,OSSpinLock已经废弃掉了,现在的自旋锁底层用的是os_unfair_lock,从底层调用看,os_unfair_lock并非忙等,会处于休眠状态.

1.6 条件锁NSConditionLock

条件锁是有条件的加锁和解锁,重要的是等待唤醒机制。因为他实现了NSLocking协议,所以他也可以想NSLock一样去加锁。
场景:多个线程操作不同的代码,去修改同一个资源,这里的代码块不是同一个代码,所以就不能简单的通过线程同步来实现,就需要使用条件锁来实现。

有两种:NSCondition、NSConditionLock

区别:

  • NSConditionLock是NSCondition的封装
  • NSConditionLock可以设置锁条件,即condition值,而NSCondition只是信号的通知

等待唤醒机制
当一个线程在执行时,将其他有相同锁的线程设置为等待状态,当自己执行结束,把其他线程设置为唤醒状态,这样就可以实现线程安全了。

1.6.1 NSCondition

API:

wait/waitUnitlData:

将当前线程设置为等待状态,知道其他线程调用signal或broadcast来唤醒,如果是waitUnitdate则等到了过了某个时间点,会自动唤醒。

signal: 将当前NSCondition对象上的正在等待的线程进行唤醒

注意:

  • 如果有多个线程处于等待状态,则随机唤醒一个线程
  • 唤醒之后并不会立即运行,而是先处于就绪状态,等CPU调度到这个线程才会运行

boadcaset: 唤醒NSCondition对象上的所有线程

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.ticketCount = 0;
    [self demo];
}

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

- (void)producer{
    [_testCondition lock]; // 操作的多线程影响
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_testCondition signal]; // 发送信号
    [_testCondition unlock];
}

- (void)consumer{
 
     [_testCondition lock];  // 操作的多线程影响
    if (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        [_testCondition wait]; // 线程等待
    }
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
     [_testCondition unlock];
}
@end

生产消费者模式.png

说明:

  • 当生产结束后发送信号,表示可以被消费了。
  • 消费者此时就可以进入消费,消费当没有产品时就需要进入等待状态,供生产者生产。

1.6.2 NSConditionLock

NSConditionLock是一把锁,一旦一个线程获得锁,其他线程一定等待。

API:

condition: 比较条件,是一个整数

lock : 加锁

  • 如果没有其他线程获得锁(不需要判断内部的condition),那他能执行后续代码,同时设置当前线程获得锁
  • 如果已经有其他线程获得锁(可能是条件锁,或者无条件锁),则等待,直到其他线程解锁

[xxx lockWhenCondition: A条件]: 带有条件的加锁

  • 在[xxx lock]的基础上,没有其他线程获得锁,且内部的condition条件满足A条件时,才会执行后续代码并让当前线程获得锁。否则依旧是等待。

[xxx unlockWithCondition: A条件] 带有条件的解锁

  • 把内部的condition设置为A条件,并broadcast广播告诉所有等待的线程已经解锁了。

return = [xxx lockWhenCondition: A条件 beforeDate: A时间]:

  • 没有其他线程获得锁,且满足A条件,且在A时间之前,可以执行后续代码并让当前线程获得锁。
  • 返回值为NO,表示没有改变锁的状态

实现

/*
 条件锁,通过条件判断来决定是否可以加锁。
 */
- (void)conditionLockTest{

    //初始化状态为2
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    
    /*
     条件为1时执行加锁,并且解锁时将条件设置为0
     */
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
         [conditionLock lockWhenCondition:1]; // conditoion = 1 内部 Condition 匹配
        NSLog(@"线程 1");
         [conditionLock unlockWithCondition:0]; // 解锁并把conditoion设置为0
    });
    
    /*
     条件为2时可执行加锁,并且解锁时将条件设置为1
     */
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       
        [conditionLock lockWhenCondition:2]; // conditoion = 2 内部 Condition 匹配
        sleep(0.1);
        NSLog(@"线程 2");
        [conditionLock unlockWithCondition:1]; // 解锁并把conditoion设置为1
    });
    
    /*
     就是普通的锁,不加任何条件,任何时候都可能来
     */
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);
       [conditionLock lock];
       NSLog(@"线程 3");
       [conditionLock unlock];
    });
}

说明:

  • 条件锁就是带有条件的,如果条件一致才可以进行加锁
  • 也可以直接使用lock不加条件,此时就可以执行了。
  • 任务2一定先执行,因为它的条件是2,,任务1需要在任务2后执行,因为任务2执行完会将条件改为1,这样任务1就可以执行了
  • 任务3可以在任何情况下执行。因为没有条件
  • 在实际使用中发现如果不给任务3前加sleep(2),往往线程3会比线程2先执行。所以猜测条件加锁要比不条件加锁更消耗性能。这样很好理解。

1.6 信号量dispatch_semaphore

介绍:
可以保证两个或多个任务不被并发调用,在进入一个任务前,线程必须先释放一个信号量,该任务完成再获取信号量,也就是进入任务前,信号量先减一,之后信号量加一。
其他想要执行该任务的线程必须等待信号量>=0才可以执行,如果小于0则说明已被锁。

信号量是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例
信号量可以有更多的取值空间,用来实现更加复杂的同步

详细使用可以查看10 - OC多线程之GCD常用API

1.7 读写锁dispatch_barrier_async

认识:
读写锁实际上是一种特殊的自旋锁,对共享资源的访问分成读者和写者,读数据可以同时读,如果是写数据就只能单个任务来写。因为如果直接加锁的话,其实读数据没必要加锁,造成了CPU的浪费
读写锁是通过栅栏函数dispatch_barrier_async来实现的。

详细的使用可以查看10 - OC多线程之GCD常用API中栅栏函数的内容。

注意:

  • 一个读写锁同时只能有一个写者或者多个读者,但不能既有读者又有写者,在读写锁保持期间也是抢占失效的
  • 如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里, 直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立

1.8 总结

  • NSLock的底层是pthread_mutex,二者性能非常接近

  • NSRecursiveLock比NSLock多了一层递归逻辑

  • NSCodition比NSLock多了一层pthread_con_init条件锁

  • NSConditionLock是在NSCondition的基础上进行的再次封装

  • 锁必须调用init方法(new内部也调用了init方法),因为init会完成底层pthread_mutex相关锁的初始化

  • 所有遵循NSLocking协议的锁类,底层都是基于pthread_mutex锁来实现的,只是封装的深度不同

  • NSLock性能接近pthread_mutex,而pthread_mutex(recursive)、NSRecursiveLock、NSCondition、NSConditionLock的耗时一个比一个高,就是由对pthread_mutex的封装深度决定的

NSLock、NSRecursiveLock、@synchronized三者的区别

NSLock:

  • 需要手动创建和释放,需要在准确的时机进行相应操作
  • 仅锁住当前线程的当前任务,无法自动实现线程间的通讯和递归问题

NSRecursiveLock:

  • 需要手动创建和释放,需要在准确的时机进行相应操作
  • 仅锁住当前线程的所有任务,无法自动实现线程间的通讯,但可以解决递归问题

@synchronized:

  • 只需将需要锁的代码都放在作用域内,确定被锁对象(被锁对象决定了锁的生命周期),@synchronized就可以做到自动创建和释放。
  • 锁住被锁对象的所有线程的所有任务,可自动实现线程间的通讯,可以解决递归问题。
    (内部逻辑为: 被锁对象可持有多个线程,每个线程可递归持有多个任务)

2、各种锁的使用场景

  • 如果只是简单的使用,例如涉及线程安全,使用NSLock即可
  • 如果是循环嵌套,推荐使用@synchronized,虽然性能要比递归锁要差,但是使用方便,而且它可以锁住所有线程的所有任务,NSRecursiveLock只能锁住当前线程的所有任务。
  • 如果在不同的代码中进行加锁,可以使用条件锁和信号量
  • 如果想要性能更好,可以使用信号量,性能仅次于自旋锁
  • 当我们想要区分读和写的操作时可以使用读写锁。

你可能感兴趣的:(11 - OC多线程之锁的认识)