iOS 线程同步

作者: Dan1els | 来源:发表于2019-07-10 11:53 被阅读0次

    多线程相关的概念

    • 时间片轮转调度算法:是目前操作系统中大量使用的线程管理方式,大致就是操作系统会给每个线程分配一段时间片(通常 100 ms 左右),这些线程都被放在一个队列中,CPU 只需要维护这个队列,当队首的线程时间片耗尽就会被强制放到队尾等待,然后提取下一个队首线程执行
    • 原子操作:“原子”一般指最小粒度,不可分割;原子操作也就是不可分割,不可中断的操作
    • 临界区 :每个进程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问
    • 忙等(busy-waiting): 试图进入临界区的线程,占着 CPU 而不释放的状态
    • 睡眠(sleep-waiting):试图进入临界区的线程,会进入睡眠状态,主动让出时间片,不会再占着 CPU 而不释放
    • 上下文切换(Context Switch):当线程进入睡眠(sleep-waiting)的时候,cpu的核心会进行上下文切换,将该线程置于等待队列中,而其他线程就会继续执行任务,上下文切换需要花费时间
    • 锁的拥有者(Lock Ownership):如果锁没有拥有者,则当它被某一条线程获取时,其他任意一条线程都可以对它进行解锁;如果锁只能有单一的拥有者,则当它被某一条线程获取时,只有这条线程可以对它进行解锁;如果锁可以有多个拥有者,则它可以同时被某多条线程获取
    • 死锁:指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局(Deadly-Embrace) ) ,若无外力作用,这些进程(线程)都将无法向前推进。一般在获得锁的线程中再次进行加锁就会发生死锁
    • 饥饿(Starvation):指一个进程一直得不到资源
    Lock Ownership.png

    线程同步方案

    要保证线程安全,就必须要线程同步,而在iOS中线程同步的方案有:

    • 原子操作
    • 信号量
    • GCD串行队列

    原子操作

    在 iOS 中,原子操作可以保证属性在单独的 setter 或者 getter 方法中是线程安全的,但是不能保证多个线程对同一个属性进行读写操作时,可以得到预期的值,也就是原子操作不保证线程安全,例如:

    // 共享资源name
    @property (copy, atomic) NSString *name;
    // 初始化
    self.name = @"A";
    
    // 线程2进行写操作,是原子操作,不可以分割的
    self.name = @"B";
    
    // 线程3进行写操作,是原子操作,不可以分割的
    self.name = @"C";
    
    // 线程4进行读操作,是原子操作,不可以分割的,但这时候存在三种可能
    self.name == @"A";
    self.name == @"B";
    self.name == @"C";
    

    Objective-C 的原子操作

    在 Objective-C 中,可以在设置属性的时候,使用 atomic 来设置原子属性,保证属性 settergetter 的原子性操作,底层是在 gettersetter 内部使用 os_unfair_lock 加锁

    @property (copy, atomic) NSString *name;
    

    Swift 的原子操作

    在 Swift 中,原生没有提供原子操作,可以使用 DispatchQueue 的同步函数来达到同样的效果

    class Person {
      // 创建一个队列
      let queue = DispatchQueue(label: "Person")
    
      // 私有化需要原子操作的属性
      private var _name: String = ""
    
      // 向外界暴露的属性,把它的 get 和 set 方法都设置为同步操作,实际上是对 _name 进行操作,这样就可以间接的对 name 进行原子操作
      var name: String {
          get {
              return queue.sync {
                  _name
              }
          }
          set {
              return queue.sync {
                  _name = newValue
              }
          }
      }
    }
    

    信号量(Semaphore)

    • 信号量(semaphore)是非负整型变量,在初始化时设置一个值 value,用来控制线程并发访问的最大数量,当 value == 1 的时候,就可以实现线程同步
    • 信号量有两个原子操作:wait()signal()
      • wait():当 value > 0,就将 value 减 1 并马上返回;当 value == 0,那当前线程就会睡眠,直到其他线程调用 signal() 把 value 加 1,当前线程恢复,然后将 value 减 1 并返回
      • signal() :将 value 加 1
      • 如果初始化的时候 value 为 0, 那么调用 wait() 方法就会马上挂起当前线程,直到别的线程调用了 signal() 方法,才会恢复
    • 被阻塞线程会进入睡眠状态
    • 信号量不支持递归
    • 信号量没有拥有者(Owner),意味着可以在一条线程进行 wait() 操作,在另外一条线程进行 signal() 操作
    • 在 iOS 中用 dispatch_semaphore 来使用信号量,也是 GCD 用来同步的一种方式
    // 初始化一个值为 5 的信号量,可以同时有 5 条线程访问临界区,其他线程则进入睡眠状态
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(5);
    
    
    // wait
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    // 临界区...
    
    // signal
    dispatch_semaphore_signal(semaphore);
    

    GCD串行队列

    • 使用 GCD 串行队列也可以达到同步的效果,配合 sync 函数就是在当前线程执行任务
    • GCD 串行队列有单一的拥有者,就是一个串行队列有对应的线程
    dispatch_queue_t queue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        // 临界区...
    });
    

    OSSpinLock

    • OSSpinLock 是一种"自旋锁"。自旋锁是一种特殊互斥锁,当一个线程需要获取自旋锁时,如果该锁已经被其他线程占用,那么会一直去请求锁,进入 忙等(busy-waiting) 状态,所以会一直占用 CPU
    • 由于自旋锁在等待锁的时候线程一直处于忙等状态,而不用进入睡眠,所以不用进行上下文切换,自旋锁的效率远高于互斥锁
    • 自旋锁适用于
      • 预计线程等待锁的时间很短
      • 临界区经常访问,但竞争情况很少发生
    • 自旋锁不安全,会出现优先级反转问题:如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 忙等 状态从而占用大量 CPU 时间片。此时低优先级线程无法与高优先级线程争夺 CPU 时间片,从而导致完成任务而无法释放锁
    • 在 iOS 10 及以上被废弃
    #import <libkern/OSAtomic.h>
    
    OSSpinLock lock = OS_SPINLOCK_INIT;
    
    // 加锁
    OSSpinLockLock(&lock);
    
    // 临界区...
    
    // 解锁
    OSSpinLockUnlock(&lock);
    

    os_unfair_lock

    • os_unfair_lock 用于取代不安全的 OSSpinLock ,iOS 10 开始支持,当一条线程等待锁的时候会进入睡眠,不再消耗 CPU 时间,当其他线程解锁以后,操作系统会激活线程
    • os_unfair_lock 有单一的拥有者
    • 这是一种不公平锁。在公平锁中,多个线程同时竞争这个锁的时候, 会考虑公平性尽可能的让不同的线程获得锁,这样会频繁进行上下文切换,牺牲性能。而在不公平锁中,系统为了减少上下文切换,当前拥有锁的线程有可能会再次获得锁,但这样做可能会让其他线程等待更长时间,造成饥饿
    #import <os/lock.h>
    
    os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
    
    
    // 加锁
    os_unfair_lock_lock(&lock);
    
    // 临界区...
    
    // 解锁
    os_unfair_lock_unlock(&lock);
    

    互斥锁

    • 互斥锁是可以看作是一种特殊的信号量,当一条线程等待锁的时候会进入睡眠状态
    • 互斥锁阻塞的过程分两个阶段,第一阶段是会先空转,可以理解成跑一个 while 循环,不断地去申请加锁,在空转一定时间之后,线程会进入睡眠状态,让出时间片,此时线程就不占用 CPU 时间片,等锁可用的时候,这个线程会立即被唤醒

    pthread_mutex

    pthread 表示 POSIX thread,是 POSIX 标准的 unix 多线程库,定义了一组跨平台的线程相关的API。pthread_mutex 是一种用 C 语言实现的互斥锁,有单一的拥有者

    #import <pthread.h>
    
    // 静态初始化
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    
    
    // 动态初始化
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
    // 初始化锁
    pthread_mutex_t mutex;
    pthread_mutex_init(mutex, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
    
    // 加锁
    pthread_mutex_lock(&mutex);
    
    // 临界区...
    
    // 解锁
    pthread_mutex_unlock(&mutex);
    
    // 销毁锁
    pthread_mutex_destroy(&_mutex);
    

    NSLock

    • NSLock 是以 Objective-C 对象的形式对 pthread_mutex 的封装,属性为 PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示
    • NSLockpthread_mutex 略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响
    • NSLock 有单一的拥有者
    NSLock *lock = [[NSLock alloc] init];
    
    // 加锁
    [lock lock];
    
    // 临界区...
    
    // 解锁
    [lock unlock];
    

    递归锁

    递归锁是一种特殊互斥锁。递归锁允许单个线程在释放之前多次获取锁,其他线程保持睡眠状态,直到锁的所有者释放锁的次数与获取它的次数相同。递归锁主要在递归迭代中使用,但也可能在多个方法需要单独获取锁的情况下使用。

    pthread_mutex(Recursive)

    pthread_mutex 支持递归锁,只要把 attr 的类型改成 PTHREAD_MUTEX_RECURSIVE 即可,它有单一的拥有者

    #import <pthread.h>
    
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化锁
    pthread_mutex_t mutex;
    pthread_mutex_init(mutex, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
    
    // 加锁
    pthread_mutex_lock(&_mutex);
    
    
    // 临界区...
    // 在同一个线程中可以多次获取锁
    
    // 解锁
    pthread_mutex_unlock(&_mutex);
    
    
    // 销毁锁
    pthread_mutex_destroy(&_mutex);
    

    NSRecursiveLock

    NSRecursiveLock 是以 Objective-C 对象的形式对 pthread_mutex(Recursive) 的封装,它有单一的拥有者

    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    
    // 加锁
    [lock lock];
    
    // 临界区...
    // 在同一个线程中可以多次获取锁
    
    // 解锁
    [lock unlock];
    

    @synchronized

    • @synchronized 是对 pthread_mutex(Recursive) 的封装,所以它支持递归加锁
    • 需要传入一个 Objective-C 对象,可以理解为把这个对象当做锁来使用
    • 实际上它是用 objc_sync_enter(id obj)objc_sync_exit(id obj) 来进行加锁和解锁
    • 底层实现:在底层存在一个全局用来存放锁的哈希表(可以理解为锁池),对传入的对象地址的哈希值作为key,去查找对应的递归锁
    • @synchronized 额外还会设置异常处理机制,性能消耗较大
    • @synchronized 有单一的拥有者
    @synchronized(lock) {
        // 临界区...
    }
    

    条件锁

    条件锁是一种特殊互斥锁,需要条件变量(condition variable) 来配合。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程。条件锁是为了解决 生产者-消费者模型

    pthread_mutex – 条件锁

    pthread_mutex 配合 pthread_cond_t,可以实现条件锁,其中 pthread_cond_t 没有拥有者

    #import <pthread.h>
    
    // 初始化锁
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, &NULL);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
    // 初始化条件变量
    pthread_cond_t cond;
    pthread_cond_init(&cond, NULL);
    
    // 消费者
    - (void)remove {
        // 加锁
        pthread_mutex_lock(&mutex);
    
        // 先判断某个条件
        if (self.data.count == 0) {
            // 如果不满足条件,则等待,具体是释放锁,用条件变量来阻塞当前线程
            // 当条件满足的时候,条件变量唤醒线程,再用原来的锁加锁
            pthread_cond_wait(&cond, &mutex);
        }
    
        [self.data removeLastObject];
    
    
        // 解锁
        pthread_mutex_unlock(&mutex);
    }
    
    
    // 生产者
    - (void)add
    {
        // 加锁
        pthread_mutex_lock(&mutex);
        
    
        [self.data addObject:@"Test"];
        
        // 信号
        // 条件变量唤醒阻塞的线程
        pthread_cond_signal(&cond);
        // 广播
        // pthread_cond_broadcast(&cond);
        
        // 解锁
        pthread_mutex_unlock(&mutex);
    }
    
    
    // 销毁
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    

    NSCondition

    NSCondition 是以 Objective-C 对象的形式对 pthread_mutexpthread_cond_t 进行了封装,NSCondition 没有拥有者

    NSCondition *condition = [[NSCondition alloc] init];
    
    // 消费者
    - (void)remove
    {
        [condition lock];
    
        
        if (self.data.count == 0) {
            // 如果不满足条件,则等待,具体是释放锁,用条件变量来阻塞当前线程
            // 当条件满足的时候,条件变量唤醒线程,再用原来的锁加锁
            [condition wait];
        }
        
        [self.data removeLastObject];
        
        [condition unlock];
    }
    
    
    // 生产者
    - (void)add
    {
        [condition lock];
        
        
        [self.data addObject:@"Test"];
        
        // 信号
        // 条件变量唤醒阻塞的线程
        [condition signal];
        
        
        [condition unlock];
    }
    

    NSConditionLock

    NSConditionLock 是对 NSCondition 的进一步封装,可以设置条件变量的值。通过改变条件变量的值,可以使任务之间产生依赖关系,达到使任务按照一定的顺序执行,它有单一的拥有者(不确定)

    // 初始化设置条件变量的为1,如果不设置则默认为0
    NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:1];
    
    
    // 消费者
    - (void)remove
    {
        // 当条件变量为2的时候加锁,否则等待
        [lock lockWhenCondition:2];
        
        [self.data removeLastObject];
        
        // 直接解锁
        [lock unlock];
    }
    
    
    // 生产者
    - (void)add
    {
        // 直接加锁
        [lock lock];
        
        
        [self.data addObject:@"Test"];
        
        
        // 解锁并让条件变量为2
        [lock unlockWithCondition:2];
    }
    

    读写锁

    读写锁是一种特殊互斥锁,提供"多读单写"的功能,多个线程可以同时对共享资源进行读取,但是同一时间只能有一条线程对共享资源进行写入

    pthread_rwlock

    pthread_rwlock 有多个拥有者

    #import <pthread.h>
    
    // 初始化
    pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;
    
    
    // 读操作
    - (void)read {
        pthread_rwlock_rdlock(&lock);
    
        // 临界区...
      
        pthread_rwlock_unlock(&lock);
    }
    
    // 写操作
    - (void)write
    {
        pthread_rwlock_wrlock(&lock);
        
        // 临界区...
        
        pthread_rwlock_unlock(&lock);
    }
    
    // 销毁
    - (void)dealloc
    {
        pthread_rwlock_destroy(&lock);
    }
    

    GCD 的 Barrier函数

    • GCD 的 Barrier 函数也可以实现"多读单写"的功能
    • Barrier 函数的作用是:等其他任务执行完毕,才会执行任务自己的任务;会执行完毕自己的任务,才会继续执行其他任务
    • 这个函数传入的并发队列必须是自己通过 dispatch_queue_cretate 创建的,如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于 dispatch_async 函数的效果
    dispatch_queue_t queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    
    
    dispatch_async(queue, ^{
        // 读
    });
    
    dispatch_async(queue, ^{
        // 读
    });
    
    
    dispatch_barrier_async(queue, ^{
        // 写
    });
    
    dispatch_async(queue, ^{
        // 读
    });
    

    性能

    性能从高到底分别是:

    • os_unfair_lock
    • OSSpinLock
    • dispatch_semaphore
    • pthread_mutex
    • GCD 串行队列
    • NSLock
    • NSCondition
    • pthread_mutex(recursive)
    • NSRecursiveLock
    • NSConditionLock
    • @synchronized

    总结:

    • OSSpinLockos_unfair_lock 性能很高,但是一个是已经废弃,一个是低级锁,苹果不建议使用低级锁
    • dispatch_semaphorepthread_mutex 也具有不错的性能,NSLockpthread_mutex 的封装,性能上接近
    • 个人建议在 Objective-C 中直接使用面向对象的 NSLock,而在 Swif t中使用 GCD 串行队列

    参考文章

    苹果官方文档

    白夜追凶,揭开iOS锁的秘密

    起底多线程同步锁(iOS)

    深入理解 iOS 开发中的锁

    相关文章

      网友评论

        本文标题:iOS 线程同步

        本文链接:https://www.haomeiwen.com/subject/pvgckctx.html