OC底层原理 - 23 iOS中的锁

引言

锁是开发中最常用的同步工具,通过锁来实现对临界资源的访问控制,从而使目标代码段同一时间只会被一个线程执行。这是一种以牺牲性能为代价的方法。

锁的实现依赖于原子操作,不同的处理器(intel、arm),不同的架构(单核、多核)实现原子操作的方式不一样。有的是通过加锁封锁总线,有的是做成单指令,有的是依据标志位,有的是依据CPU相关的指令对,总之,不同的机制可以实现原子操作。

原子操作,就像原子一样不可再分割的操作,即:一个操作(有可能包含多个子操作)只要开始执行,在执行完毕前,不会被其它操作或者指令中断。原子操作解决了多线程不安全问题中的原子性问题。如果没有原子操作的话,操作可能会因为中断异常等各种原因引起数据状态的不一致,从而影响到程序的正确性。

iOS中的atomic属性修饰符的语义就是原子操作。被atomic所修饰的属性,确保了setter和getter的原子性,这使得setter和getter这两个方法是线程安全的,但是对于整个对象来说,不一定是线程安全的。并且atomic比nonatomic开销要大很多,所以一般考虑到性能时,会将属性修饰符设置为nonatomic。

虽然锁是同步两个线程的有效办法,但是获取锁是一个相对昂贵的操作,即使在无争用的情况下,也是如此。相比之下,许多原子操作只需要花费一小部分时间就可以完成,并且可以像锁一样有效。

使用锁可以保证多线程操作共享数据时的安全问题,却也降低了程序的执行效率。锁的这种机制无法彻底避免以下几点问题:
① 锁引起的线程阻塞,对于没有能占用到锁的线程或者进程将会一直等待锁的占有者释放资源后才能继续。
② 申请和释放锁的操作增加了很多访问共享资源的消耗。
③ 锁不能很好的避免编程开发者设计实现的程序出现死锁或者活锁的可能。
④ 优先级反转和锁护送怪现象。
⑤ 难以调试。

锁的分类

锁的分类多种多样,根据线程的状态可以分为:互斥锁自旋锁

互斥锁:互斥锁充当资源周围的保护屏障,如果多个线程竞争同一个互斥锁,每次只允许一个线程访问。如果一个互斥锁正在使用中,另一个线程试图获取它,该线程就会阻塞,进入睡眠状态,直到该互斥锁被它的持有者释放后再将其唤醒。注意:互斥锁阻塞的线程处于休眠状态

自旋锁:如果一个自旋锁正在使用中,另一个线程试图获取它时,该线程不会进入睡眠状态,而是反复轮询其锁条件,直到该条件为真。这适用于竞争预期较低的情况。注意:自旋锁阻塞的线程处于忙等状态

使线程进入睡眠状态,主动让出时间片并不代表效率高,因为操作系统切换到另一个线程上下文时,通常需要10ms,而且需要切换两次。因此,如果锁的预期等待时间很短,轮询通常比线程休眠更有效。

iOS中的锁

转载自iOS多线程编程(七) 同步机制与锁

pthread_mutex 互斥锁

互斥锁是一种用来防止多个线程同一时刻对共享资源进行访问的信号量,它的原子性确保了如果一个线程锁定了一个互斥量,将没有其他线程在同一时间可以锁定这个互斥量。它的唯一性确保了只有它解锁了这个互斥量,其他线程才可以对其进行锁定。当一个线程锁定一个资源的时候,其他对该资源进行访问的线程将会被挂起,直到该线程解锁了互斥量,其他线程才会被唤醒,进一步才能锁定该资源进行操作。

pthread_mutex是POSIX提供的互斥锁,基于C语言实现,可跨平台。基本上OC层面的互斥锁都是基于pthread_mutex实现的。主要的函数如下:

// 宏定义。用于静态的mutex的初始化,采用默认的attr。
PTHREAD_MUTEX_INITIALIZER 
// 用于动态的mutex的初始化,第二个参数为mutex的属性attr
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 
// 请求获得锁,如果当前mutex未被持有,则加锁成功;
// 如果当前mutex已被持有,那么请求加锁线程不会获得成功,并阻塞线程,直到mutex被释放
int pthread_mutex_lock(pthread_mutex_t *mutex); 
// 释放锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); 
// 尝试获得锁,如果当前mutex已经被持有或者不可用,这个函数就直接return,不会阻塞当前线程
int pthread_mutex_trylock(pthread_mutex_t *mutex); 
// 销毁mutex锁,并且释放所有它所占有的资源
int pthread_mutex_destroy(pthread_mutex_t *mutex); 

使用pthread_mutex的主要过程为:

  • ① 创建pthread_mutex;
  • ② 使用pthread_mutex_lock加锁,使用pthread_mutex_unlock解锁;
  • ③ 销毁pthread_mutex;

创建pthread_mutex:

初始化pthread_mutex有两种方式,一种是通过宏定义(PTHREAD_MUTEX_INITIALIZER)获得默认的互斥锁,另一种是通过函数(pthread_mutex_init )创建锁。如果不需要自定义pthread_mutex的属性信息,使用宏定义的方式更快速便捷。

使用pthread_mutex_lock加锁与pthread_mutex_unlock解锁

pthread_mutex(互斥锁)利用排他性来保证线程安全,在同一时刻只允许一个线程获得锁。如果一个线程已经获得互斥锁,另一个线程就无法访问,直到锁的持有者正确的释放了互斥锁,另一个线程才有机会获得锁。

- (void)pthread_mutexDemo {
  
    // 创建mutex
    __block pthread_mutex_t mutex = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
    // 线程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        pthread_mutex_lock(&mutex);
        NSLog(@"执行任务A---%@",[NSThread currentThread]);
        sleep(5);
        NSLog(@"任务A执行完毕---%@",[NSThread currentThread]);
        pthread_mutex_unlock(&mutex);
    });
    // 线程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);// 让线程1的任务先执行
        pthread_mutex_lock(&mutex);
        NSLog(@"执行任务B---%@",[NSThread currentThread]);
        pthread_mutex_unlock(&mutex);
    });
    
    // 销毁mutex:确保mutex使用完毕再销毁
//    pthread_mutex_destroy(&mutex);
}

// 打印结果:
2021-02-27 21:00:14.034887+0800 lockDemo[83241:6444414] 执行任务A---<NSThread: 0x600000fad000>{number = 5, name = (null)}
2021-02-27 21:00:19.039718+0800 lockDemo[83241:6444414] 任务A执行完毕---<NSThread: 0x600000fad000>{number = 5, name = (null)}
2021-02-27 21:00:19.040232+0800 lockDemo[83241:6444416] 执行任务B---<NSThread: 0x600000faee80>{number = 3, name = (null)}

本例中,线程1先获得互斥锁,尽管线程2的异步任务在sleep(1)后就可执行,但此时线程1已持有互斥锁,所以再次遇到pthread_mutex_lock(&mutex)时,必须等待,此时线程2处于阻塞态,直到 sleep(5)后线程1释放互斥锁,线程2才被唤醒继续执行任务。

使用pthread_mutex_trylock

除了pthread_mutex_lock函数外,pthread_mutex还提供了pthread_mutex_trylock 函数,与pthread_mutex_lock不同的是,使用pthread_mutex_trylock 函数来申请加锁,不管是否能获得锁都立即返回,并不阻塞线程。如果申请失败则返回错误:EBUSY(锁尚未解除)或者EINVAL(锁变量不可用)。一旦在trylock的时候有错误返回,那就把前面已经拿到的锁全部释放,然后过一段时间再来一遍。

如果将上例中线程2的pthread_mutex_lock(&mutex)操作,换成pthread_mutex_trylock(&mutex)。则结果为

2021-02-27 21:04:54.976015+0800 lockDemo[62208:9380951] 执行任务A---<NSThread: 0x6000017de040>{number = 6, name = (null)}
2021-02-27 21:04:55.977173+0800 lockDemo[62208:9380952] 执行任务B---<NSThread: 0x6000017a5980>{number = 4, name = (null)}
2021-02-27 21:04:59.980902+0800 lockDemo[62208:9380951] 任务A执行完毕---<NSThread: 0x6000017de040>{number = 6, name = (null)}

注意事项

  • 避免多次申请锁或释放未获得的锁

使用pthread_mutex时,pthread_mutex_lock与pthread_mutex_unlock要成对使用,一般情况下,一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致异常。一定要确保在正确的时机获得锁和释放锁。

  • 避免阻塞

假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,同时也不可能释放锁。

  • 避免死锁

如果两个线程存在互相等待释放锁的情况,也会导致死锁的发生。

  • 记得pthread_mutex_destroy销毁锁,但要确保pthread_mutex已使用完毕。

pthread_mutex(recursive) 递归锁

在实际开发中,有可能存在这样的需求,递归调用或需要重复的获得锁。这种情况下,如果使用pthread_mutex(互斥锁)就会阻塞线程,任务也就无法继续执行。这就需要使用递归锁来解决问题了。

递归锁是互斥锁的变体。递归锁允许单个线程在释放锁之前多次获取该锁(可重入,保存了锁的次数信息)。而不会阻塞当前线程,其他线程仍然处于阻塞状态,直到锁的持有者以获得锁的相同次数释放锁。

递归锁主要在递归迭代期间使用,也可以在多个方法分别需要获得锁的情况下使用。

递归锁的使用:

pthread_mutex维护了以下几种锁类型:

/*
 * Mutex type attributes
 */
#define PTHREAD_MUTEX_NORMAL        0     // 普通互斥锁
#define PTHREAD_MUTEX_ERRORCHECK    1     // 检查锁
#define PTHREAD_MUTEX_RECURSIVE     2     // 递归锁
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL

PTHREAD_MUTEX_NORMAL是默认属性的互斥锁;与PTHREAD_MUTEX_DEFAULT等同。
PTHREAD_MUTEX_ERRORCHECK 查错锁:以损失些许性能的方式返回错误信息;
PTHREAD_MUTEX_RECURSIVE就是递归锁;

可以通过pthread_mutexattr_t属性设置锁的类型,示例代码如下:

- (void)pthread_mutex_recursiveDemo {
    
    // init attr
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
//    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);

    // init mutex
    __block pthread_mutex_t mutex_recursive;
    pthread_mutex_init(&mutex_recursive, &attr);
    pthread_mutexattr_destroy(&attr);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
        static void (^RecursiveMethod)(int);
        RecursiveMethod = ^(int value) {
            // lock
            pthread_mutex_lock(&mutex_recursive);
            if (value > 0) {
                NSLog(@"value = %d,thread = %@",value,[NSThread currentThread]);
                RecursiveMethod(value - 1);
            }else{
                pthread_mutex_destroy(&mutex_recursive);
            }
            // unlock
            pthread_mutex_unlock(&mutex_recursive);
        };
        
        RecursiveMethod(5);
    });
    //    使用完毕后,销毁
    //    pthread_mutex_destroy(& mutex_recursive);
}

// 打印结果:
2021-02-27 21:18:33.418542+0800 lockDemo[83366:6460107] value = 5,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}
2021-02-27 21:18:33.418716+0800 lockDemo[83366:6460107] value = 4,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}
2021-02-27 21:18:33.418845+0800 lockDemo[83366:6460107] value = 3,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}
2021-02-27 21:18:33.419116+0800 lockDemo[83366:6460107] value = 2,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}
2021-02-27 21:18:33.419250+0800 lockDemo[83366:6460107] value = 1,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}

注意: pthread_mutex(recursive)只保证在单线程情况下可重入,当多个线程获取相同的pthread_mutex(recursive)锁会导致死锁的发生。

pthread_rwlock(读写锁)

基本上所有的问题都可以用互斥的方案去解决,但是可以解决并不代表适合。

pthread_mutex(互斥锁)有个缺点,就是只要锁住了,不管其他线程要干什么,都不允许进入临界区。设想这样一种情况:临界区变量a正在被线程1读取,加了个mutex锁,线程2如果也要读变量a,因为被线程1加了个互斥锁,就只能等待线程1读取完毕。但事实情况是,读取数据并不影响数据内容本身,所以即便被1个线程读着,另外一个线程也应该被允许去读。除非另外一个线程是写操作,为了避免数据不一致的问题,写线程就需要等读线程都结束了再写。

因此诞生了读写锁,有的地方也叫共享-独占锁

读写锁的特性是这样的,当一个线程加了读锁访问临界区,另外一个线程也想访问临界区读取数据的时候,也可以加一个读锁,这样另外一个线程就能够成功进入临界区进行读操作了。此时读锁线程有两个。当第三个线程需要进行写操作时,它需要加一个写锁,这个写锁只有在读锁的拥有者为0时才有效。也就是等前两个读线程都释放读锁之后,第三个线程就能进去写了。总结一下就是:

  • 当读写锁被一个线程以读模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程还可以继续进行。
  • 当读写锁被一个线程以写模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程也被阻塞。

这样更精细的控制,就能减少mutex导致的阻塞延迟时间。如果受保护的数据结构经常被读取,并且只偶尔修改,则可以显著提高性能。虽然用mutex也能起作用,但这种场合,明显读写锁更好。

pthread中读写锁主要函数如下:

// 静态初始化方法
PTHREAD_RWLOCK_INITIALIZER
// 动态初始化,可传pthread_rwlockattr_t属性
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
// 销毁 pthread_rwlock
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 获得读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 尝试获得读锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
// 获得写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 尝试获得写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 释放锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

使用读写锁与pthread_mutex类似,都是通过初始化创建锁,之后根据读写不同场景进行加锁、解锁操作,在使用完毕后别忘了销毁锁。示例代码如下:

- (void)pthread_rwlock_demo {
    pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
    _rwlock = rwlock;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 读
        [self readWithTag:1];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 读
        [self readWithTag:2];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 写
        [self writeWithTag:3];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 写
        [self writeWithTag:4];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 读
        [self readWithTag:5];
    });
    //使用完毕后销毁锁:不可在未使用完毕前销毁
    //pthread_rwlock_destroy(&_rwlock);
}

- (void)readWithTag:(NSInteger )tag {
    pthread_rwlock_rdlock(&_rwlock);
    NSLog(@"start read ---- %ld",tag);
    self.path = [[NSBundle mainBundle] pathForResource:@"pthread_rwlock" ofType:@".txt"];
    self.content = [NSString stringWithContentsOfFile:self.path encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"end   read ---- %ld",tag);
    pthread_rwlock_unlock(&_rwlock);
}

- (void) writeWithTag:(NSInteger)tag {
    pthread_rwlock_wrlock(&_rwlock);
    NSLog(@"start wirte ---- %ld",tag);
    [self.content writeToFile:self.path atomically:YES encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"end   wirte ---- %ld",tag);
    pthread_rwlock_unlock(&_rwlock);
}

// 打印结果 :  读操作可共享,写操作互斥
2021-02-27 21:29:44.081500+0800 lockDemo[82462:10201536] start read ---- 2
2021-02-27 21:29:44.081500+0800 lockDemo[82462:10201541] start read ---- 1
2021-02-27 21:29:44.081795+0800 lockDemo[82462:10201536] end   read ---- 2
2021-02-27 21:29:44.081795+0800 lockDemo[82462:10201541] end   read ---- 1
2021-02-27 21:29:44.082017+0800 lockDemo[82462:10201537] start wirte ---- 3
2021-02-27 21:29:44.082182+0800 lockDemo[82462:10201537] end   wirte ---- 3
2021-02-27 21:29:44.082351+0800 lockDemo[82462:10201535] start wirte ---- 4
2021-02-27 21:29:44.082459+0800 lockDemo[82462:10201535] end   wirte ---- 4
2021-02-27 21:29:44.082617+0800 lockDemo[82462:10201538] start read ---- 5
2021-02-27 21:29:44.082799+0800 lockDemo[82462:10201538] end   read ---- 5

注意事项

避免写线程饥饿
由于读写锁的性质,在默认情况下是很容易出现写线程饥饿的。因为它必须要等到所有读锁都释放之后,才能成功申请写锁。比如在写线程阻塞的时候,有很多读线程是可以一个接一个地在那儿插队的(在默认情况下,只要有读锁在,写锁就无法申请,然而读锁可以一直申请成功,就导致所谓的插队现象),那么写线程就不知道什么时候才能申请成功写锁了,然后它就饿死了。所以要注意锁建立后的优先级问题。不过不同系统的实现版本对写线程的优先级实现不同。Solaris下面就是写线程优先,其他系统默认读线程优先。

pthread_cond (条件变量)

当我们在使用多线程的时候,有时一把只会lock和unlock的锁未必就能完全满足我们的使用。因为普通的锁只能关心锁与不锁,而不在乎用什么钥匙(满足什么条件)才能开锁,而我们在处理资源共享的时候,有时候需要只有满足一定条件的情况下才能打开这把锁。

这时候,POSIX提供的pthread_cond(条件变量)就派上了用场。主要的函数如下:

// 静态初始化
PTHREAD_COND_INITIALIZER
// 动态初始化并允许设置属性
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
// 发送信号(给指定线程)
int pthread_cond_signal(pthread_cond_t *cond);
// 广播信号(给所有线程)
int pthread_cond_broadcast(pthread_cond_t *cond);
// 等待信号
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 等待信号,如果在指定时间仍未收到信号,则返回
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mute

条件变量可以做到让一个线程等待多个线程的结束,并在合适的时候唤醒正在等待的线程,具体是什么时候,取决于你设置的条件是否满足。

示例代码如下:

pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean        ready_to_go = false;
 
void MyCondInitFunction()
{
    mutex =  (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
    pthread_cond_init(&condition, NULL);
}
 
void MyWaitOnConditionFunction()
{
    // Lock the mutex.
    pthread_mutex_lock(&mutex);
    // If the predicate is already set, then the while loop is bypassed;
    // otherwise, the thread sleeps until the predicate is set.
    while(ready_to_go == false)
    {
        pthread_cond_wait(&condition, &mutex);
    }
    
    // Do work. (The mutex should stay locked.)
   
    // Reset the predicate and release the mutex.
    ready_to_go = false;
    pthread_mutex_unlock(&mutex);
}

void SignalThreadUsingCondition()
{
    // At this point, there should be work for the other thread to do.
    pthread_mutex_lock(&mutex);
    
    ready_to_go = true;
    // Signal the other thread to begin work.
    pthread_cond_signal(&condition);
    pthread_mutex_unlock(&mutex);
}

- (void)pthread_cont_demo {
    MyCondInitFunction();
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        MyWaitOnConditionFunction();
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        SignalThreadUsingCondition();
    });
}

补充一下,原则上pthread_cond_signal是只通知一个线程,pthread_cond_broadcast是用于通知很多线程。但POSIX标准也允许让pthread_cond_signal用于通知多个线程,不强制要求只允许通知一个线程。具体看各系统的实现。

另外,在调用pthread_cond_wait之前,必须要申请互斥锁,当线程通过pthread_cond_wait进入waiting状态时,会释放传入的互斥锁。

NSLock (互斥锁)

NSLockCocoa 基于pthread_mutex实现的一个基本的互斥锁。对应pthread_mutex的PTHREAD_MUTEX_ERRORCHECK的类型。遵循NSLocking协议,该协议定义了lock和unlock方法。通过lockunlock来进行锁定和解锁。

实际上,OC层面的基于pthread_mutex封装的锁对象都遵循NSLocking协议,这样设计的目的是因为,对于这些锁的锁定与解锁行为对于底层的操作是一致的。使用这些方法来获取和释放锁,就像使用任何pthread_mutex一样。

除了NSLocking协议提供的标准锁定行为,NSLock类还添加了tryLocklockBeforeDate:方法。

  • tryLock方法尝试获取该锁,但如果该锁不可用,并不会阻塞,该方法只返回NO。

  • lockBeforeDate:方法尝试获取锁,但是如果在指定Date的时间限制内没有获得锁,则会解除线程阻塞(并返回NO)。

使用示例如下:

- (void)nslock_demo {
    //主线程
    NSLock *lock = [[NSLock alloc] init];
    //线程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [lock lock];
        NSLog(@"线程1任务 开始");
        sleep(2);
        NSLog(@"线程1任务 结束");
        [lock unlock];
    });
    //线程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        if ([lock tryLock]) {//尝试获取锁,如果获取不到返回NO,不会阻塞该线程
            NSLog(@"线程2尝试获取锁,锁可用");
            [lock unlock];
        }else{
            NSLog(@"线程2尝试获取锁,锁不可用");
        }
        
        NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];
        if ([lock lockBeforeDate:date]) {//尝试在未来的3s内获取锁,并阻塞该线程,如果3s内获取不到恢复线程, 返回NO,不会阻塞该线程
            NSLog(@"没有超时,线程2获得锁");
            [lock unlock];
        }else{
            NSLog(@"超时,线程2没有获得锁");
        }
    });
}

// 打印结果:
2021-02-27 21:44:10.071157+0800 lockDemo[36464:983765] 线程1任务 开始
2021-02-27 21:44:11.074331+0800 lockDemo[36464:983761] 线程2尝试获取锁,锁不可用
2021-02-27 21:44:12.074832+0800 lockDemo[36464:983765] 线程1任务 结束
2021-02-27 21:44:12.075065+0800 lockDemo[36464:983761] 没有超时,线程2获得锁

NSRecursiveLock (递归锁)

NSRecursiveLock是Cocoa对pthread_mutex互斥锁 PTHREAD_MUTEX_RECURSIVE类型的封装。与pthread_mutex(递归锁)一样,主要是用在循环或递归操作中。该锁可以被同一个线程多次获取,而不会被阻塞。它记录了成功获得锁的次数,每一次成功的获得锁,都必须有一个配套的释放锁与其对应,只有当所有的加锁和解锁调用都被平衡后,锁才会被实际释放,以便其他线程能够获取它。

除了实现NSLocking协议的方法外,NSRecursiveLock还提供了两个方法,分别如下:

// 在给定的时间之前去尝试请求一个锁
- (BOOL)lockBeforeDate:(NSDate *)limit

// 尝试去请求一个锁,并会立即返回一个布尔值,表示尝试是否成功
- (BOOL)tryLock

使用示例如下:

- (void)NSRecursiveLock_demo {
    //主线程
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
    //线程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void(^MyRecursiveFunction)(int);
        MyRecursiveFunction = ^(int value)
        {
            [recursiveLock lock];
            if (value > 0)
            {
                NSLog(@"递归任务1--%d",value);
                sleep(2);
                --value;
                MyRecursiveFunction(value);
            }
            [recursiveLock unlock];
        };
        MyRecursiveFunction(5);
    });
    //线程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        [recursiveLock lock];
        NSLog(@"任务2");
        [recursiveLock unlock];
    });
}

// 打印结果如下:
2021-02-27 21:48:33.853605+0800 lockDemo[83298:10293307] 递归任务1--5
2021-02-27 21:48:35.856179+0800 lockDemo[83298:10293307] 递归任务1--4
2021-02-27 21:48:37.859868+0800 lockDemo[83298:10293307] 递归任务1--3
2021-02-27 21:48:39.863572+0800 lockDemo[83298:10293307] 递归任务1--2
2021-02-27 21:48:41.868646+0800 lockDemo[83298:10293307] 递归任务1--1
2021-02-27 21:48:43.870858+0800 lockDemo[83298:10293303] 任务2

注意:由于递归锁只有在所有锁操作与解锁操作得到平衡后才会被释放,长时间持有任何锁会导致其他线程阻塞,直到递归完成。如果可以通过重写代码来消除递归,或者消除使用递归锁的需要,那么可能会获得更好的性能。

NSCondition (条件)

NSCondition是对POSIX条件pthread_cond的封装, 它将所需的锁和条件数据结构包装在一个对象中。使得开发者可以像锁定互斥锁一样锁定它,然后像等待条件一样等待它。

NSConditionNSLock、@synchronized等是不同的是,NSCondition可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。其它线程也能上锁,而之后可以根据条件决定是否继续运行线程,即线程是否要进入 waiting 状态.

除了实现NSLocking协议的方法外,NSCondition还提供了以下函数:

- (void)wait;   // 等待信号
- (BOOL)waitUntilDate:(NSDate *)limit;  // 等待信号,如果limit时间已到,则直接返回
- (void)signal; // 发送信号
- (void)broadcast; // 广播信号

通过NSCondition可以实现不同线程的调度。一个线程被某一个条件所阻塞,直到另一个线程满足该条件从而发送信号给该线程使得该线程可以正确的执行。

- (void)NSCondition_demo {
    __block NSInteger timeToDoWork = 0;
    NSCondition *cocoaCondition = [[NSCondition alloc] init];
    // 线程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [cocoaCondition lock];
        while (timeToDoWork <= 0){
            [cocoaCondition wait];
        }
         
        timeToDoWork--;
         
        // Do real work here.
         
        [cocoaCondition unlock];
    });
    
    // 线程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);
        [cocoaCondition lock];
        timeToDoWork++;
        [cocoaCondition signal];
        [cocoaCondition unlock];
    });
}

NSConditionLock (条件锁)

NSConditionLock是对NSCondition的进一步封装,条件锁对象所定义的互斥锁可以用特定的值(某个条件)锁定和解锁。除了NSLocking协议外,NSConditionLock还提供如下函数与属性:

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;  // 当condition的值满足条件时 获取锁
- (BOOL)tryLock; // 尝试获得锁,不管是否获得成功都立即返回,不阻塞线程
- (BOOL)tryLockWhenCondition:(NSInteger)condition; // 当condition的值满足条件时,尝试加锁
- (void)unlockWithCondition:(NSInteger)condition; // 释放锁,并将condition的值修改为执行值
- (BOOL)lockBeforeDate:(NSDate *)limit; // 在指定时间限制内获取锁,获取失败,返回NO
// 在指定时间内,当condition的值满足条件时获取锁
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

通常,当线程需要以特定的顺序执行任务时,比如当一个线程生产数据另一个线程消耗数据时,可以使用NSConditionLock对象。在生产者执行时,可以通过特定的条件获得锁(条件本身只是定义的一个整数值),当生产者完成时,它将解锁,并将锁的条件设置为可以唤醒消费者线程的条件。

下面的示例展示了如何使用条件锁处理生产者-消费者问题。假设一个应用程序包含一个数据队列。生产者线程向队列中添加数据,消费者线程从队列中提取数据。生成器不需要等待特定的条件,但是它必须等待锁可用,这样它才能安全地将数据添加到队列中。

NSMutableArray *products = [NSMutableArray array];
NSConditionLock *lock = [[NSConditionLock alloc] init];
NSInteger HAS_DATA = 1;
NSInteger NO_DATA = 0;
    
dispatch_async(dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_DEFAULT), ^{
       
    while (1) {
        [lock lockWhenCondition:NO_DATA];
        [products addObject:[[NSObject alloc] init]];
        NSLog(@"produce a product, 总量: %zi", products.count);
        [lock unlockWithCondition:HAS_DATA];
        sleep(1);
    }
});
    
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
    while (1) {
        NSLog(@"wait for product");
        [lock lockWhenCondition:HAS_DATA];
        [products removeObjectAtIndex:0];
        NSLog(@"custome a product");
        [lock unlockWithCondition:NO_DATA];
    }
});

当生产者释放锁的时候,把条件设置成了1。这样消费者可以获得该锁,进而执行程序,如果消费者获得锁的条件和生产者释放锁时给定的条件不一致,则消费者永远无法获得锁,也不能执行程序。同样,如果消费者释放锁给定的条件和生产者获得锁给定的条件不一致的话,则生产者也无法获得锁,程序也不能执行。

注意

  1. unlock 与 unlockWithCondition:(NSInteger)condition 的区别:
    • unlock:释放锁但并不改变condition的值;
    • unlockWithCondition:释放锁,并将condition的值修改为指定值。
  2. 由于在实现操作系统时的细微参与,即使代码里没有实际发出信号,条件锁也允许以虚假的成功返回。为了避免由这些假信号引起的问题,您应该始终将谓词与条件锁结合使用。谓词是确定线程继续执行是否安全的更具体的方法。这个条件只是让线程处于休眠状态,直到发送信号的线程可以设置谓词。

@sychronized

@sychronized是使用起来最简单的互斥锁,通常只需要@sychronized(obj)这样一个简单的指令就可以实现加/解锁操作。

- (void)sychronized_demo {
    NSObject *obj = [[NSObject alloc] init];
    NSObject *obj1 = [[NSObject alloc] init];
    //线程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized(obj){
            NSLog(@"任务1");
            sleep(5);
        }
    });
    //线程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized(obj){
            NSLog(@"任务2");
        }
    });
}
// 打印结果:
2021-02-27 22:08:25.288126+0800 lockDemo[83702:10333558] 任务1
2021-02-27 22:08:30.291985+0800 lockDemo[83702:10333557] 任务2

@synchronized指令使用传入的对象(obj)作为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程2中的@synchronized(obj)改为@synchronized(obj1),线程2就不会被阻塞。

// 如果将线程2的 @synchronized(obj)换成 @synchronized(obj1),则
2021-02-27 22:09:42.831004+0800 lockDemo[83783:10344549] 任务1
2021-02-27 22:09:42.831014+0800 lockDemo[83783:10344546] 任务2

同时@synchronized还允许重入,前面提到的pthread_mutex(递归锁)和NSRecursiveLock也支持重入,但它们只允许在同一线程内多次重入,而@synchronized支持多线程重入。这是因为@sychronized内部,除了维护了同一线程的加锁次数lockCount外,还维护了使用唯一标识的线程数threadCount

@synchronized指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。

注意:确保传入@synchronized的obj不为nil,因为如果传入的obj为nil的话,实际上并不会做任何实际的内容,也无法达到加锁的目的。

dispatch_semaphore信号量

dispatch_semaphoreNSCondition类似,都是一种基于信号的同步方式,但NSCondition信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效)。而 dispatch_semaphore能保存发送的信号。dispatch_semaphore 的核心是 dispatch_semaphore_t 类型的信号量。

dispatch_semaphore是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。

与其相关的主要有三个函数:

  • dispatch_semaphore_t dispatch_semaphore_create(long value)
    输出一个dispatch_semaphore_t类型且值为value的信号量。值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。

  • long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
    这个函数会使传入的信号量dsema的值加1;

  • long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
    这个函数会使传入的信号量dsema的值减1;
    这个函数的作用是这样的,如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。

示例代码如下:

dispatch_semaphore_t signal = dispatch_semaphore_create(1);
    dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_semaphore_wait(signal, overTime);
            NSLog(@"需要线程同步的操作1 开始");
            sleep(2);
            NSLog(@"需要线程同步的操作1 结束");
        dispatch_semaphore_signal(signal);
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        dispatch_semaphore_wait(signal, overTime);
            NSLog(@"需要线程同步的操作2");
        dispatch_semaphore_signal(signal);
    });
    
//执行结果为:
需要线程同步的操作1 开始
需要线程同步的操作1 结束
需要线程同步的操作2

OSSpinLock 自旋锁

OSSpinLock是一把自旋锁,性能很高。因为它一直是do while忙等状态。这种自旋锁的缺点是当等待时会消耗大量CPU资源,所以它不适用于较长时间的任务。

OSSpinLock是整数类型,约定是解锁为零,锁定为非零。锁必须自然对齐,并且不能在缓存抑制的内存中。

如果锁已经被持有,OSSpinLockLock()将自旋,但会使用各种各样的策略来后退,使其对大多数优先级反转活锁免疫。但因为它可以旋转,所以在某些情况下可能效率低下。

如果锁被持有,OSSpinLockTry()立即返回false,如果它获得了锁,则返回true。它不自旋。 OSSpinLockUnlock()通过置零无条件地解锁锁。

OSSpinLock 示例

- (void)osspinlock_demo {
    __block OSSpinLock theLock = OS_SPINLOCK_INIT;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(&theLock);
        NSLog(@"线程1");
        sleep(5);
        OSSpinLockUnlock(&theLock);
        NSLog(@"线程1解锁成功");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        OSSpinLockLock(&theLock);
        NSLog(@"线程2");
        OSSpinLockUnlock(&theLock);
    });
}
// 打印结果
2021-02-27 22:05:13.526 ThreadLockControlDemo[2856:316247] 线程1
2021-02-27 22:05:23.528 ThreadLockControlDemo[2856:316247] 线程1解锁成功
2021-02-27 22:05:23.529 ThreadLockControlDemo[2856:316260] 线程2

OSSpinLock 问题

新版iOS中,系统维护了5个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了OSSpinLock。

具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于OSSpinLock的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放OSSpinLock。

所以从iOS10.0开始,苹果弃用了OSSpinLock,并用os_unfair_lock进行替代。不过,os_unfair_lock的实现属于互斥锁,当锁被占用的时候,线程处于阻塞状态,而非忙等。

iOS中的锁的性能

在iOS中,各种锁的性能如下图所示:


16239819199365.jpg

从图中可以看出,在iOS中的锁性能从高往底依次是:

  • OSSpinLock(自旋锁)
  • dispatch_semaphone(信号量)
  • pthread_mutex(互斥锁)
  • NSLock(互斥锁)
  • NSCondition(条件锁)
  • pthread_mutex(recursive 互斥递归锁)
  • NSRecursiveLock(递归锁)
  • NSConditionLock(条件锁)
  • synchronized(互斥锁)

性能总结:

  1. OSSpinLock自旋锁由于安全性问题,在iOS10之后已经被废弃,其底层的实现用os_unfair_lock替代
    • 使用OSSpinLock会处于忙等待状态
    • 使用os_unfair_lock会处于休眠状态
  2. atomic原子锁自带一把自旋锁,只能保证setter、getter时的线程安全,在日常开发中使用更多的还是nonatomic修饰属性
    • atomic:当属性在调用setter、getter方法时,会加上自旋锁OSSpinLock,用于保证同一时刻只能有一个线程调用属性的读或写,避免了属性读写不同步的问题。由于是底层编译器自动生成的互斥锁代码,会导致效率相对较低
    • nonatomic:当属性在调用setter、getter方法时,不会加上自旋锁,即线程不安全。由于编译器不会自动生成互斥锁代码,可以提高效率
  3. @synchronized在底层维护了一个哈希表进行线程data的存储,通过链表表示可重入(即嵌套)的特性,虽然性能较低,但由于简单好用,使用频率很高
  4. NSLock、NSRecursiveLock底层是对pthread_mutex的封装
  5. NSConditionNSConditionLock是条件锁,底层都是对pthread_mutex的封装,当满足某一个条件时才能进行操作,和信号量dispatch_semaphore类似
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容