美文网首页iOS底层原理整理
iOS 多线程(2)-线程安全(锁)

iOS 多线程(2)-线程安全(锁)

作者: 周灬 | 来源:发表于2019-07-26 22:51 被阅读0次

    1. 关于锁的讨论

    说到锁,大家可能对于这个问题都比较迷茫,都有什么锁,每个锁都有什么作用,我们在开发中应该在对应的情况下应该使用什么种类的锁?接下来我们大家一起研究一下这些问题,我们先来看下为什么要使用锁。

    多线程是一把双刃剑,他即可以提高我们的运行效率,但是当资源共享多个线程同时存取同一块资源的时候,可能会造成引发数据错乱和数据安全问题。

    安全隐患分析.png

    通过上图我们发现,当线程A访问数据并对数据进行操作的同时,线程B访问的数据还是没有更新的数据,线程B同样对数据进行操作,当两个线程结束返回时,就会发生数据错乱的问题。
    那遇到这样的数据错乱的问题我们应该怎么解决这种数据的错乱问题,我们想的是只要我们在操作数据从开始到结束的过程中没有线程对数据进行的任何增删改查的操作,所以我们得把这段过程加上一把锁,不让别人去操作就可以了.

    安全隐患解决.png

    我们再看一个实际的例子,下面通过一个售票实例来看一下线程安全的重要性;

    #import "ViewController.h"
    
    @interface ViewController ()
    
    @property(nonatomic,strong)NSThread *thread01;
    @property(nonatomic,strong)NSThread *thread02;
    @property(nonatomic,strong)NSThread *thread03;
    @property(nonatomic,assign)NSInteger numTicket;
    
    //@property(nonatomic,strong)NSObject *obj;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // 总票数为30
        self.numTicket = 30;
    //开启线程的方式有很多,此处用NSThread开启.
        self.thread01 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
        self.thread01.name = @"售票员01";
        self.thread02 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
        self.thread02.name = @"售票员02";
        self.thread03 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
        self.thread03.name = @"售票员03";
    }
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        [self.thread01 start];
        [self.thread02 start];
        [self.thread03 start];
    }
    // 售票
    -(void)saleTicket
    {
        while (1) {
            // 创建对象
            // self.obj = [[NSObject alloc]init];
            // 锁对象,本身就是一个对象,所以self就可以了
            // 锁定的时候,其他线程没有办法访问这段代码
            @synchronized (self) {
                // 模拟售票时间,我们让线程休息0.05s 
                [NSThread sleepForTimeInterval:0.05];
                if (self.numTicket > 0) {
                    self.numTicket -= 1;
                    NSLog(@"%@卖出了一张票,还剩下%zd张票",[NSThread currentThread].name,self.numTicket);
                }else{
                    NSLog(@"票已经卖完了");
                    break;
                }
            }
        }
    }
    @end
    

    当没有加锁的时候我们看一下输出

    没有加锁的输出.png

    我们发现第29张,第27张都被销售了3次,这显然是不允许的,这就是数据错乱.

    2. 线程锁

    为了避免出现数据错乱这种情况,我们需要用到工具线程锁。

    一. 线程锁有以下几个基本类型:

    ①. 自旋锁: atomic 即OSSpinLock 在ios中已经不是线程安全的了,如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。(效率最高,如果一直等不到锁会较占用cpu资源)
    ②. 信号锁:dispatch_semaphore是gcd中通过信号量来实现共享数据的数据安全。(效率第二)
    ③. 互斥锁:pthread_mutex ,nslock ,synchronized都是互斥锁。如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。(synchronized效率最低)
    ④. 递归锁:pthread_mutex(recursive)与NSRecursiveLock , 多次调用不会阻塞已获取该锁的线程。
    ⑤. 条件锁:nsconditionlock 满足一定的条件的加锁和解锁,可以实现依赖关系。俗称线程依赖nscondition条件锁,也是通过信号量来解锁,主要用来实现生产者消费者模式。

    二. 线程锁的几种同步方案

    ①. OSSpinLock
    ②. os_unfair_lock
    ③. pthread_mutex
    ④. dispatch_semaphore
    ⑤. dispatch_queue(DISPATCH_QUEUE_SERIAL)
    ⑥. NSLock
    ⑦. NSRecursiveLock
    ⑧. NSCondition
    ⑨. NSConditionLock
    ⑩. @synchronized
    注意:加锁之后一定要记得解锁,避免造成死锁的情况.

    ①.OSSpinLock(自旋锁)

    OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态(进入死循环等待属性解锁),一直占用着CPU资源.目前已经不再安全,可能会出现优先级反转问题.如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁.一旦解锁立即读取资源,因此效率高.
    使用时应该导入头文件#import <libkern/OSAtomic.h>.

    //能否共用一把锁的关键在于事件之间是否可以同时进行.
    //初始化
    OSSpinLock lock = OS_SPINLOCK_INIT;
    //加锁
    OSSpinLockLock(&lock);
    //解锁
    OSSpinLockUnlock(&lock);
    //尝试加锁
    OSSpinLockTry(&lock);
    

    demo:

    #import "OSSpinLockDemo.h"
    #import <libkern/OSAtomic.h>
    @interface OSSpinLockDemo()
    @property (assign, nonatomic) OSSpinLock ticketLock;
    @end
    
    @implementation OSSpinLockDemo
    
    - (instancetype)init
    {
    self = [super init];
    if (self) {
    self.ticketLock = OS_SPINLOCK_INIT;
    }
    return self;
    }
    
    
    //卖票
    - (void)sellingTickets{
    OSSpinLockLock(&_ticketLock);
    
    [super sellingTickets];
    
    OSSpinLockUnlock(&_ticketLock);
    }
    
    @end
    

    OSSpinLock在iOS10.0以后就被弃用了,可以使用os_unfair_lock_lock替代。

    ②. os_unfair_lock(互斥锁)

    os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持.从底层调用看,等待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);
    

    demo

    #import "os_unfair_lockDemo.h"
    #import <os/lock.h>
    @interface os_unfair_lockDemo()
    @property (assign, nonatomic) os_unfair_lock ticketLock;
    @end
    
    @implementation os_unfair_lockDemo
    - (instancetype)init
    {
    self = [super init];
    if (self) {
    self.ticketLock = OS_UNFAIR_LOCK_INIT;
    }
    return self;
    }
    
    
    //卖票
    - (void)sellingTickets{
    os_unfair_lock_lock(&_ticketLock);
    
    [super sellingTickets];
    
    os_unfair_lock_unlock(&_ticketLock);
    }
    @end
    
    ③. pthread_mutex(互斥锁)

    mutex叫做”互斥锁”,等待锁的线程会处于休眠状态.
    使用的时候需要导入头文件#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);
    /*
     * 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_init(mutex, &attr);
    //尝试加锁
    pthread_mutex_trylock(&mutex);
    // 加锁
    pthread_mutex_lock(&mutex);
    //解锁
    pthread_mutex_unlock(&mutex);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    //销毁锁
    pthread_mutex_destroy(&mutex);
    

    备注:我们可以不初始化属性,在传属性的时候直接传NULL,表示使用默认属性PTHREAD_MUTEX_NORMAL。pthread_mutex_init(mutex, NULL);
    demo

    #import "pthread_mutexDemo.h"
    #import <pthread.h>
    @interface pthread_mutexDemo()
    @property (assign, nonatomic) pthread_mutex_t ticketMutex;
    @end
    
    @implementation pthread_mutexDemo
    
    - (instancetype)init
    {
    self = [super init];
    if (self) {
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
    // 初始化锁
    pthread_mutex_init(&(_ticketMutex), &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
    
    }
    return self;
    }
    
    //卖票
    - (void)sellingTickets{
    pthread_mutex_lock(&_ticketMutex);
    
    [super sellingTickets];
    
    pthread_mutex_unlock(&_ticketMutex);
    }
    @end
    

    这个代码暑促的时候会造成死锁,所以我们稍微修改一下代码.

    //卖票
    - (void)sellingTickets{
    pthread_mutex_lock(&_ticketMutex);
    [super sellingTickets];
    [self sellingTickets2];
    pthread_mutex_unlock(&_ticketMutex);
    }
    
    
    - (void)sellingTickets2{
    pthread_mutex_lock(&_ticketMutex);
    NSLog(@"%s",__func__);
    pthread_mutex_unlock(&_ticketMutex);
    }
    
    ④. pthread_mutex(属性修改为PTHREAD_MUTEX_RECURSIVE) – 递归锁

    上面的代码就会造成线程死锁,因为方法sellingTickets的结束需要sellingTickets2解锁,方法sellingTickets2的结束需要sellingTickets解锁,相互引用造成死锁.
    但是pthread_mutex_t里面有一个属性可以解决这个问题PTHREAD_MUTEX_RECURSIVE
    PTHREAD_MUTEX_RECURSIVE递归锁:允许同一个线程对同一把锁进行重复加锁。要考重点同一个线程和同一把锁.

    - (instancetype)init
    {
    self = [super init];
    if (self) {
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化锁
    pthread_mutex_init(&(_ticketMutex), &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
    
    }
    return self;
    }
    

    对于上面的问题还有一个解决方案就是在方法sellingTickets2中重新在创建一把新的锁,两个方法的锁对象不同,就不会造成线程死锁了。

    ⑤. pthread_cond – 条件锁

    可以通过条件锁可以解决线程之间的依赖的问题.(例如生产者消费者的问题)

    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化锁
    pthread_mutex_init(&_mutex, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
    // 初始化条件
    pthread_cond_t condition
    pthread_cond_init(&_cond, NULL);
    
    // 等待条件
    pthread_cond_wait(&_cond, &_mutex);
    
    //激活一个等待该条件的线程
    pthread_cond_signal(&_cond);
    //激活所有等待该条件的线程
    pthread_cond_broadcast(&_cond);
    
    //销毁资源
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
    

    使用案例:假设我们有一个数组,里面有两个线程,一个是添加数组,一个是删除数组,我们先调用删除数组,在调用添加数组,但是在数组为空的时候不调用删除数组。

    #import "pthread_mutexDemo1.h"
    #import <pthread.h>
    
    @interface pthread_mutexDemo1()
    @property (assign, nonatomic) pthread_mutex_t mutex;
    @property (assign, nonatomic) pthread_cond_t cond;
    @property (strong, nonatomic) NSMutableArray *data;
    @end
    
    @implementation pthread_mutexDemo1
    
    - (instancetype)init
    {
    if (self = [super init]) {
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化锁
    pthread_mutex_init(&_mutex, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
    // 初始化条件
    pthread_cond_init(&_cond, NULL);
    
    self.data = [NSMutableArray array];
    }
    return self;
    }
    - (void)otherTest
    {
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
    }
    
    // 线程1
    // 删除数组中的元素
    - (void)__remove
    {
    pthread_mutex_lock(&_mutex);
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
    // 等待
    pthread_cond_wait(&_cond, &_mutex);
    }
    
    [self.data removeLastObject];
    NSLog(@"删除了元素");
    
    pthread_mutex_unlock(&_mutex);
    }
    
    // 线程2
    // 往数组中添加元素
    - (void)__add
    {
    pthread_mutex_lock(&_mutex);
    
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    
    // 激活一个等待该条件的线程
    pthread_cond_signal(&_cond);
    
    pthread_mutex_unlock(&_mutex);
    }
    
    - (void)dealloc
    {
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
    }
    
    ⑥. NSLock

    NSLock是对mutex普通锁的封装。相当于pthread_mutex_init(mutex, NULL);.
    NSLock 遵循 NSLocking协议。Lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO.

    @protocol NSLocking
    - (void)lock;
    - (void)unlock;
    @end
    
    @interface NSLock : NSObject <NSLocking> {
    @private
    void *_priv;
    }
    
    - (BOOL)tryLock;
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    @property (nullable, copy) NSString *name
    @end
    

    demo:

    #import "LockDemo.h"
    @interface LockDemo()
    @property (strong, nonatomic) NSLock *ticketLock;
    @end
    @implementation LockDemo
    //卖票
    - (void)sellingTickets{
    [self.ticketLock lock];
    [super sellingTickets];
    [self.ticketLock unlock];
    }
    
    @end
    
    ⑦. NSRecursiveLock(递归锁)

    NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致.

    #import "RecursiveLockDemo.h"
    @interface RecursiveLockDemo()
    @property (nonatomic,strong) NSRecursiveLock *ticketLock;
    @end
    @implementation RecursiveLockDemo
    //卖票
    - (void)sellingTickets{
    [self.ticketLock lock];
    [super sellingTickets];
    [self.ticketLock unlock];
    }
    @end
    
    ⑧. NSCondition

    NSCondition是对mutex和cond的封装,更加面向对象,我们使用起来也更加的方便简洁.

    @interface NSCondition : NSObject <NSLocking> {
    - (void)wait;
    - (BOOL)waitUntilDate:(NSDate *)limit;
    - (void)signal;
    - (void)broadcast;
    @property (nullable, copy) NSString *name 
    @end
    

    demo

    // 线程1
    // 删除数组中的元素
    - (void)__remove
    {
    [self.condition lock];
    if (self.data.count == 0) {
    // 等待
    [self.condition wait];
    }
    [self.data removeLastObject];
    NSLog(@"删除了元素");
    [self.condition unlock];
    }
    
    // 线程2
    // 往数组中添加元素
    - (void)__add
    {
    [self.condition lock];
    sleep(1);
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    // 信号
    [self.condition signal];
    [self.condition unlock];
    }
    
    ⑨. NSConditionLock

    NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值.

    @interface NSConditionLock : NSObject <NSLocking> {
     
    - (instancetype)initWithCondition:(NSInteger)condition;
    
    @property (readonly) NSInteger condition;
    - (void)lockWhenCondition:(NSInteger)condition;
    - (BOOL)tryLock;
    - (BOOL)tryLockWhenCondition:(NSInteger)condition;
    - (void)unlockWithCondition:(NSInteger)condition;
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
    @property (nullable, copy) NSString *name;
    @end
    

    里面有三个常用的方法:

    1. initWithCondition:初始化Condition,并且设置状态值.
    2. lockWhenCondition: (NSInteger)condition:当状态值为condition的时候加锁.
    3. unlockWithCondition: (NSInteger)condition当状态值为condition的时候解锁.

    demo

    @interface NSConditionLockDemo()
    @property (strong, nonatomic) NSConditionLock *conditionLock;
    @end
    @implementation NSConditionLockDemo
    - (instancetype)init
    {
    if (self = [super init]) {
    self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    }
    return self;
    }
    
    - (void)otherTest
    {
    [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
    }
    
    - (void)__one
    {
    [self.conditionLock lock];
    NSLog(@"__one");
    sleep(1);
    [self.conditionLock unlockWithCondition:2];
    }
    
    - (void)__two
    {
    [self.conditionLock lockWhenCondition:2];
    NSLog(@"__two");
    [self.conditionLock unlockWithCondition:3];
    }
    @end
    
    ⑩. dispatch_semaphore
    • semaphore叫做”信号量”.
    • 信号量的初始值,可以用来控制线程并发访问的最大数量.
    • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步.
    //表示最多开启5个线程
    dispatch_semaphore_create(5);
    // 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
    // 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    // 让信号量的值+1
    dispatch_semaphore_signal(self.semaphore);
    
    @interface dispatch_semaphoreDemo()
    @property (strong, nonatomic) dispatch_semaphore_t semaphore;
    @end
    @implementation dispatch_semaphoreDemo
    - (instancetype)init
    {
    if (self = [super init]) {
    self.semaphore = dispatch_semaphore_create(1);
    }
    return self;
    }
    - (void)otherTest
    {
    for (int i = 0; i < 20; i++) {
    [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
    }
    }
    - (void)test
    {
    // 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
    // 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(2);
    NSLog(@"test - %@", [NSThread currentThread]);
    
    // 让信号量的值+1
    dispatch_semaphore_signal(self.semaphore);
    }
    @end
    

    我们在运行代码打印的时候发现,每隔一秒出现一次打印。虽然我们同时开启20个线程,但是一次只能访问一条线程的资源

    ⑪. dispatch_queue(串行队列)

    使用GCD的串行队列也可以实现线程同步的.

    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
    // 追加任务1
    for (int i = 0; i < 2; ++i) {
    NSLog(@"1---%@",[NSThread currentThread]);
    }
    });
    
    dispatch_sync(queue, ^{
    // 追加任务2
    for (int i = 0; i < 2; ++i) {
    NSLog(@"2---%@",[NSThread currentThread]);
    }
    });
    
    @synchronized(递归锁)

    @synchronized是对mutex递归锁的封装, @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作.

    //卖票
    - (void)sellingTickets{
    @synchronized ([self class]) {
    [super sellingTickets];
    }
    }
    

    对是实现底层我们可以在objc4的objc-sync.mm文件中找到 synchronized就是在开始和结束的时候调用了objc_sync_enter&objc_sync_exit方法。
    objc_sync_enter实现

    int objc_sync_enter(id obj)
    {
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
    SyncData* data = id2data(obj, ACQUIRE);
    assert(data);
    data->mutex.lock();
    } else {
    // @synchronized(nil) does nothing
    if (DebugNilSync) {
    _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
    }
    objc_sync_nil();
    }
    
    return result;
    }
    

    就是根据id2data方法找到一个data对象,然后在对data对象进行mutex.lock()加锁操作。我们点击进入id2data方法继续查找.

    #define LIST_FOR_OBJ(obj) sDataLists[obj].data
    static StripedMap<SyncList> sDataLists;
    

    发现获取data对象的方法其实就是根据sDataLists[obj].data这个方法来实现的,也就是一个哈希表。

    三. 线程锁的对比

    1.线程锁的对比

    我们对一般的情况下各种不同锁的性能做对比.

    性能对比:

    1. os_unfair_lock
    2. OSSpinLock
    3. dispatch_semaphore
    4. pthread_mutex
    5. dispatch_queue(DISPATCH_QUEUE_SERIAL)
    6. NSLock
    7. NSCondition
    8. pthread_mutex(recursive)
    9. NSRecursiveLock
    10. NSConditionLock
    11. @synchronized
    2.自旋锁和互斥锁的对比

    通过上面对于锁的介绍我们可知11种锁中只有OSSpinLock是自旋锁,他的替代方案os_unfair_lock也是互斥锁,既然是自旋锁和互斥锁的比较也就是OSSpinLock和其他种类的锁作比较.

    自旋锁使用起来比较划算的方案 互斥锁使用起来比较划算的方案
    1.预计线程等待锁的时间很短.
    2.加锁的代码(临界区)经常被调用,
    但竞争情况很少发生.
    3.CPU资源不紧张.
    4.多核处理器
    1.预计线程等待锁的时间较长.
    2.单核处理器.
    3.临界区有IO操作.
    临界区代码复杂或者循环量大.
    临界区竞争非常激烈.
                                想了解更多iOS学习知识请联系:QQ(814299221)

    相关文章

      网友评论

        本文标题:iOS 多线程(2)-线程安全(锁)

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