美文网首页
关于iOS线程锁的一点研究

关于iOS线程锁的一点研究

作者: 优质胡萝北 | 来源:发表于2020-12-31 00:07 被阅读0次

    什么是锁

    多线程中,对共享资源进行访问,为了防止并发引起的相关问题,通常都是引入锁的机制来处理并发问题。学术上对线程锁有好几种不同的定义方式,这里要对锁的几个概念做一个解释。

    1. 临界区

      指的是一块对公共资源进行访问的代码,并非一种机制或是算法。

    2. 阻塞锁和非阻塞锁

      阻塞锁和非阻塞锁的区别,线程访问临界区时,该资源上锁与否线程是否被挂起。阻塞锁会挂起线程,等待临界区解锁,而非阻塞锁会保持活跃状态。

    3. 递归锁和非递归锁

      递归锁和非递归锁的区别,当一个线程多次获取同一个递归锁时,线程不会产生死锁。但是一个线程多次获取同一个非递归锁,则会产生死锁。从效率层面上来说,非递归锁的效率高于递归锁

    4. 死锁

      死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

    自旋锁

    非阻塞锁 非递归锁

    自旋锁(Spin Lock),它的工作原理是当某个线程需要访问临界区时,如果该临界区已经被上锁,那么该线程不会被挂起,而是会循环请求线程锁,此时线程处于忙等的状态(在非耗时操作下,这种忙等是可以接受的),直到该资源被解锁释放。

    线程挂起主动出让时间片的做法是有性能消耗的,这种上下文切换会通常占用10μs。所以非阻塞锁是性能最高的锁。

    iOS系统下可用的自旋锁:

    • OSSpinlock:iOS10以后被废弃,有可能造成死锁,参考ibireme的文章,不再安全的 OSSpinLock
    • os_unfair_lock:iOS10之后支持,解决了OSSpinlock优先级反转的问题。从底层看线程处于休眠状态,并非处于忙等,该锁实现原理有待考证
    #import <os/lock.h>
    
    {
        os_unfair_lock_t unfairLock;
        unfairLock = &OS_UNFAIR_LOCK_INIT;
        os_unfair_lock_lock(unfairLock);
        ...
        os_unfair_lock_unlock(unfairLock);
    }
    

    互斥锁

    阻塞锁 递归锁 非递归锁

    互斥锁(Mutex),它的工作原理是当某个线程访问临界区已经被加锁,那么该线程会进入休眠状态。当临界区解锁,则等待线程会被唤醒。互斥锁要保证在任一时刻,只能有一个线程访问临界区,同时只有上锁线程能够进行unLock操作

    iOS系统下可用的互斥锁:

    • pthread_mutex:C语言实现的互斥锁,可以指定是否是递归锁,效率高。Foundation框架下实现的锁基本都是基于它封装的
    #import <pthread.h>
    
    
    /*
     * PTHREAD_MUTEX_NORMAL     默认非递归锁
     * PTHREAD_MUTEX_ERRORCHECK 非递归锁
     * PTHREAD_MUTEX_RECURSIVE  递归锁
     * PTHREAD_MUTEX_DEFAULT    PTHREAD_MUTEX_NORMAL
     */
    
    {
        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_mutex_lock(&lock);
        ...
        pthread_mutex_unlock(&lock);
            
        pthread_mutexattr_destroy(&attr);
        pthread_mutex_destroy(&lock);
    }
    
    
    • NSLock:OC对象封装的非递归锁
    #import <Foundation/Foundation.h>
    
    {
        NSLock *lock = [[NSLock alloc] init];
        [lock lock];
        ...
        [lock unlock];
    }
    
    
    • NSRecursiveLock:OC对象封装的递归锁
    #import <Foundation/Foundation.h>
    
    {
        NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
        [lock lock];
        ...
        [lock unlock];
    }
    
    
    • @synchronized:牺牲了效率换来语法上简洁的互斥锁,递归锁
    {
        @synchronized (NSObject.new) {
            ...
        }
    }
    

    条件锁

    阻塞锁 非递归锁

    条件锁(Condition Lock),实际上是对一个互斥锁和一个条件变量的封装。当线程想要访问临界区,需要满足Condition

    iOS系统下可用的互斥锁:

    • NSCondition
    • NSConditionLock

    信号量

    信号量(Semaphore)是实现异步调度的一种策略,这种机制可以实现线程加锁的目的。信号量机制与互斥锁最大的区别,是互斥锁要保证统一时间只能有一个线程访问临界区,但是信号量可以任意指定同时访问临界区的线程数

    iOS在GCD中封装了dispatch_semaphore,用于实现信号量调度

    1. dispatch_semaphore_create(long value)
      初始化dispatch_semaphore_t类型的信号量,参数value是最大并发量。注意value须大于0,否则会返回null。

    2. dispatch_semaphore_signal(dispatch_semaphore_t signal)
      参数signal是传入所需信号量,并使传入的信号量加1,可以理解为解锁。

    3. dispatch_semaphore_wait(dispatch_semaphore_t signal, dispatch_time_t timeout)
      参数是传入一个信号量和一个超时时间。当传入的信号量的值大于0(可执行并发),会继续执行临界区代码,并且将传入的信号量减1。当传入的信号量的值等于0(无可并发资源),则线程进入休眠状态主动让出时间片,并将该临界区任务加入等待队列,待信号量加1时,执行队列顶部任务。如果在线程休眠的过程中一直没有收到信号直到timeOut,则线程会继续访问临界区。可以理解为加锁。

    使用代码如下:

    {
        dispatch_semaphore_t lock = dispatch_semaphore_create(1);   
        dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);       
        dispatch_semaphore_signal(lock);
    }
    

    总结

    • 具体使用哪一种锁,要根据不同的业务场景和功能性需求进行选择
    • 在保证没有递归获取并且线程优先级一致的情况下,临界区非耗时操作可以选择自旋锁
    • 如果不能保证访问临界区线程优先级相同,并且要求对数据的原子性操作,那么推荐使用互斥锁,这里建议尽量使用非递归锁,首先是效率上较高并且在发生死锁的时候容易Debug
    • 如果想要控制最大并发,允许多线程访问临界区,可以使用信号量控制
    • 推荐重点学习掌握pthread_mutexdispatch_semaphore

    相关文章

      网友评论

          本文标题:关于iOS线程锁的一点研究

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