iOS随笔iOS Developer拓展层

iOS 之 线程锁整理

2019-06-28  本文已影响0人  陌路卖酱油

前言

最开始我想把线程和线程锁放在一起整理出一篇文章,结果整理了线程发现有点长,于是便把线程锁单独拿出来了。感兴趣的小伙伴也可以去看下线程的生命周期,NSThread、GCD、NSOperation的使用与总结,因为两篇文章原本是想放在一起的。


正文

一、锁的一些概念和性能对比

1.1 为什么要使用锁(线程安全)

线程安全是指,当一个线程访问数据的时候,其他的线程不能对其进行访问,直到该线程访问完毕。简单来讲就是在同一时刻,对同一个数据操作的线程只有一个。只有确保了这样,才能使数据不会被其他线程影响。而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。

举例来说:现在仅剩余一张火车票,每一个购票请求都是一个线程,那么同一时刻有多个线程同时请求出票,那么剩余的这一张票将会同时出票多次,这明显是不合理的,所以锁的出现,就是为了确保线程安全问题。

1.2 锁的一些概念
1.3 性能对比
锁的性能比较.png
//10000000
OSSpinLock:                 112.38 ms
dispatch_semaphore:         160.37 ms
os_unfair_lock:             208.87 ms
pthread_mutex:              302.07 ms
NSCondition:                320.11 ms
NSLock:                     331.80 ms
pthread_rwlock:             360.81 ms
pthread_mutex(recursive):   512.17 ms
NSRecursiveLock:            667.55 ms
NSConditionLock:            999.91 ms
@synchronized:             1654.92 ms
//1000
OSSpinLock:                   0.02 ms
dispatch_semaphore:           0.03 ms
os_unfair_lock:               0.04 ms
pthread_mutex:                0.06 ms
NSLock:                       0.06 ms
pthread_rwlock:               0.07 ms
NSCondition:                  0.07 ms
pthread_mutex(recursive):     0.09 ms
NSRecursiveLock:              0.12 ms
NSConditionLock:              0.18 ms
@synchronized:                0.33 ms

二、锁的使用(种类)

上锁有两种方式trylocklock:当前线程锁失败,也可以继续其它任务,用 trylock 合适;当前线程只有锁成功后,才会做一些有意义的工作,那就 lock,没必要轮询 trylock
注:以下大部分锁都会提供trylock接口,不再作解释。

2.1 OSSpinLock (自旋锁)
需要导入头文件
#import <libkern/OSAtomic.h>
// 初始化
OSSpinLock spinLock = OS_SPINLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
OSSpinLockTry(&spinLock)

以GCD为例,代码以及执行结果如下:

2.1.1 OSSpinLockLock
// OSSpinLockLock
- (void)UseOSSpinLock{
    __block OSSpinLock spinLock = OS_SPINLOCK_INIT;
    dispatch_queue_t queue = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        OSSpinLockLock(&spinLock);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程1  第 %d 次",i);
        }
        OSSpinLockUnlock(&spinLock);
    });

    dispatch_async(queue, ^{
        OSSpinLockLock(&spinLock);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程2  第 %d 次",i);
        }
        OSSpinLockUnlock(&spinLock);
    });

    dispatch_async(queue, ^{
        OSSpinLockLock(&spinLock);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程3  第 %d 次",i);
        }
        OSSpinLockUnlock(&spinLock);
    });
}
OSSpinLockLock

我们可以看到,我们用的并发异步线程,但是加锁之后,执行结果并没有并发异步执行。

2.1.2 OSSpinLockTry
// OSSpinLockTry
- (void)UseOSSpinLock{
    __block OSSpinLock spinLock = OS_SPINLOCK_INIT;
    dispatch_queue_t queue = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        OSSpinLockTry(&spinLock);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程4  第 %d 次",i);
        }
        OSSpinLockUnlock(&spinLock);
    });

    dispatch_async(queue, ^{
        OSSpinLockTry(&spinLock);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程5  第 %d 次",i);
        }
        OSSpinLockUnlock(&spinLock);
    });

    dispatch_async(queue, ^{
        OSSpinLockTry(&spinLock);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程6  第 %d 次",i);
        }
        OSSpinLockUnlock(&spinLock);
    });
}
OSSpinLockTry-1

执行结果可以看出来,OSSpinLockTry并没有阻塞线程。也符合上面所说:当前线程锁失败,也可以继续其它任务。但是这只是测试一下,项目中不要这么写,因为这样没有意义,可以如下:

//OSSpinLockTry
- (void)UseOSSpinLock{
    __block OSSpinLock spinLock = OS_SPINLOCK_INIT;
    dispatch_queue_t queue = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        if(OSSpinLockTry(&spinLock)){
            for(int i = 0; i < 3; i++){
                sleep(1);
                NSLog(@"线程7  第 %d 次",i);
            }
            OSSpinLockUnlock(&spinLock);
        }else{
            NSLog(@"锁7失败,执行一些其他事情");
        }
    });

    dispatch_async(queue, ^{
        if(OSSpinLockTry(&spinLock)){
            for(int i = 0; i < 3; i++){
                sleep(1);
                NSLog(@"线程8  第 %d 次",i);
            }
            OSSpinLockUnlock(&spinLock);
        }else{
            NSLog(@"锁8失败,执行一些其他事情");
        }
    });

    dispatch_async(queue, ^{
        if(OSSpinLockTry(&spinLock)){
            for(int i = 0; i < 3; i++){
                sleep(1);
                NSLog(@"线程9  第 %d 次",i);
            }
            OSSpinLockUnlock(&spinLock);
        }else{
            NSLog(@"锁9失败,执行一些其他事情");
        }
    });
    
}
OSSpinLockTry-2

执行结果可以看出,加锁失败后执行另一部分代码,并没有自旋去等待加锁,执行后其他锁释放也不会再次加锁,所以用的时候要考虑场景。

2.2 os_unfair_lock(互斥锁)

os_unfair_lock网上很多文章,有说它是自旋锁的,但是官方文档中有一段是这样的:

This is a replacement for the deprecated OSSpinLock. This function doesn't spin on contention, but instead waits in the kernel to be awoken by an unlock.

自我理解为:“这是对已弃用的osspinlock的替换。这个函数不会在争用时自旋,而是在内核中等待解锁来唤醒。”所以,它应该是互斥锁,并不是自旋锁。

需要导入头文件
#import <os/lock.h>
// 初始化
 os_unfair_lock unfair_lock = OS_UNFAIR_LOCK_INIT;
// 加锁
os_unfair_lock_lock(&unfair_lock);
// 解锁
os_unfair_lock_unlock(&unfair_lock);
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
os_unfair_lock_trylock(&unfair_lock);
/*
注:解决不同优先级的线程申请锁的时候不会发生优先级反转问题.
不过相对于 OSSpinLock , os_unfair_lock性能方面减弱了许多.
*/

使用方法上同,不做示范。

2.3 dispatch_semaphore (信号量)

信号量,是持有计数的信号。个人觉得有点类似于引用计数:create时定义最大线程数,使用时wait进行计数-1,结束时signal进行计数+1,当计数大于零时可执行,等于零时阻塞线程进行等待执行。
信号量的作用,个人觉得有以下几点:

// 初始化
dispatch_semaphore_t semaphore_t = dispatch_semaphore_create(1);
// 加锁
dispatch_semaphore_wait(semaphore_t,DISPATCH_TIME_FOREVER);
// 解锁
dispatch_semaphore_signal(semaphore_t);
2.3.1 最大并发数

不多说,直接上代码:

//2.dispatch_semaphore
- (void)UseSemaphore{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程1 第%d次 线程:%@",i,[NSThread currentThread]);
        }
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程2 第%d次 线程:%@",i,[NSThread currentThread]);
        }
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程3 第%d次 线程:%@",i,[NSThread currentThread]);
        }
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        for(int i = 0; i < 3; i++){
            sleep(1);
            NSLog(@"线程4 第%d次 线程:%@",i,[NSThread currentThread]);
        }
        dispatch_semaphore_signal(semaphore);
    });
}
最大并发数
通过结果我们可以看到,一共是四个并发异步线程,但是由于设置信号量,间接控制了最大并发数。值得注意的是:最大并发数,是指执行任务的线程最多是两个(信号量设置的是两个),但是,处于回收状态的线程不算此列.也就是说,执行任务的时候不只有两个线程,还有处于回收状态的线程,所以子线程个数不为2;
2.3.2 线程安全

线程不安全时:

- (void)UseSemaphoreLock{
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    self.number = 50;
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue, ^{
        for(int i = 0; i < 3; i++){
            [weakSelf changeNumber];
        }
    });
    dispatch_async(queue, ^{
        for(int i = 0; i < 3; i++){
            [weakSelf changeNumber];
        }
    });
}

- (void)changeNumber{
    _number = _number - 1;
    sleep(1);
    NSLog(@"number == %ld",_number);
}
线程不安全时

我们可以看到,如果异步线程,同时更改同意资源时,那么可能出现数据混乱。所以我们可以用信号量加锁,保证线程安全:

- (void)UseSemaphoreLock{
    self.semaphore1 = dispatch_semaphore_create(1);
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    self.number = 50;
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue, ^{
        for(int i = 0; i < 3; i++){
            [weakSelf changeNumber];
        }
    });
    dispatch_async(queue, ^{
        for(int i = 0; i < 3; i++){
            [weakSelf changeNumber];
        }
    });
}

- (void)changeNumber{
    //相当于加锁
    dispatch_semaphore_wait(_semaphore1, DISPATCH_TIME_FOREVER);
    _number = _number - 1;
    sleep(1);
    NSLog(@"number == %ld",_number);
    //相当于解锁
    dispatch_semaphore_signal(_semaphore1);
}
线程安全时
如上,用信号量可以处理线程安全问题。(当然我们也可以把waitsignal加到异步线程当中,但是觉得那么做的话,实际上还是控制了最大并发数,并不是解决线程安全。)
2.3.3 线程同步
//semaphore实现线程同步
- (void)semaphoreSync {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任务1
        [NSThread sleepForTimeInterval:1];              // 模拟耗时操作
        NSLog(@"任务1 %@",[NSThread currentThread]);      // 打印当前线程
        number = 100;
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end1,number = %d",number);
    dispatch_async(queue, ^{
        // 追加任务2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"任务2 %@",[NSThread currentThread]);      // 打印当前线程
        
        number = 50;
        
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end2,number = %d",number);
    dispatch_async(queue, ^{
        // 追加任务3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"任务3 %@",[NSThread currentThread]);      // 打印当前线程
        number = 10;
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end3,number = %d",number);
}
同步
可以看到,虽然我们建立的并行异步线程,但是执行结果却是同步执行。原因如下:在主线程程中设置信号量为0dispatch_semaphore_create(0);当执行到追加任务1的子线程时,进入子线程,主线程继续执行dispatch_semaphore_wait,但是此时信号量为0,dispatch_semaphore_wait在此处阻塞主线程进入等待状态,直到任务1的子线程执行dispatch_semaphore_signal使信号量+1,此时主线程中处于等待的dispatch_semaphore_wait可以使信号量-1,于是停止阻塞线程并继续向下执行。(利用阻塞线程实现线程同步)
2.4 pthread_mutex(互斥锁)
需要导入头文件
#import <pthread/pthread.h>
//声明锁
pthread_mutex_t mutex_t;
// 初始化(两种)
1.普通初始化
pthread_mutex_init(&mutex_t, NULL); 
2.宏初始化
pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
// 加锁
pthread_mutex_lock(&mutex_t);
// 解锁
pthread_mutex_unlock(&mutex_t);
// 尝试加锁,可以加锁时返回的是 0,否则返回一个错误
pthread_mutex_trylock(& mutex_t)
// 释放锁
pthread_mutex_destroy(&_lock)
pthread_mutex_init(&mutex_t, NULL);
初始化锁 NULL等同于PTHREAD_MUTEX_DEFAULT

PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。

PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。

PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。

PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列

持续更新中..........

上一篇下一篇

猜你喜欢

热点阅读