iOS底层原理探索—多线程的“锁”

探索底层原理,积累从点滴做起。大家好,我是Mars。

往期回顾

iOS底层原理探索 — OC对象的本质
iOS底层原理探索 — class的本质
iOS底层原理探索 — KVO的本质
iOS底层原理探索 — KVC的本质
iOS底层原理探索 — Category的本质(一)
iOS底层原理探索 — Category的本质(二)
iOS底层原理探索 — 关联对象的本质
iOS底层原理探索 — block的本质(一)
iOS底层原理探索 — block的本质(二)
iOS底层原理探索 — Runtime之isa的本质
iOS底层原理探索 — Runtime之class的本质
iOS底层原理探索 — Runtime之消息机制
iOS底层原理探索 — RunLoop的本质
iOS底层原理探索 — RunLoop的应用
iOS底层原理探索 — 多线程的本质
iOS底层原理探索 — 多线程的经典面试题

前言

多线程是iOS开发中很重要的一个环节,无论是开发过程还是在面试环节中,多线程出现的频率都非常高。我们会通过几篇文章的探索,深入浅出的分析多线程技术。

我们通过之前两篇文章的分析,大致对多线程技术有了一定的了解,认识到了多线程技术的强大。但是多线程在应用过程中存在一定的安全隐患,今天我们继续分析,来看多线程中如何解决这些问题。

多线程的安全隐患

当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。
比如多个线程访问同一个对象、同一个变量、同一个文件。
具体场景为:存钱取钱、购买车票等。
我们模拟一下卖票的场景。一共有15张票,开设三个窗口卖票,每个窗口卖掉5张,我们来看一下多个线程访问的时候会出现什么问题:

/** 卖1张票 */
- (void)saleTicket {
    int oldTicketsCount = self.ticketsCount;
    sleep(.2);
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    
    NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
}

/** 模拟卖票 */
- (void)ticketTest {
    self.ticketsCount = 15;
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 窗口一
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    // 窗口二
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    // 窗口三
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

我们打印一下执行结果:


iOS底层原理探索—多线程的“锁”_第1张图片
模拟卖票.png

我们发现,经过15次卖票后,正常来说15张票已经全部卖完才对,但是最后一次打印还剩5张票。这就出现了线程安全问题。

我们再来看下面一个例子:

iOS底层原理探索—多线程的“锁”_第2张图片
多线程安全隐患.png

例如上图中,一个 integer类型的对象值为17,当线程A和线程B同时访问到的时候,线程A做+1操作,同时线程B也做+1操作。

由于两个线程访问到integer类型对象时的值都为17,分别做+1操作后变量的值变为18。但是实际的结果应为做了两次+1操作,值应该为19。这就出现了问题。

那么多线程同样提供了解决方案:使用线程同步技术。

线程同步技术

常见的线程同步技术是加锁
我们再讲上面的例子优化:

iOS底层原理探索—多线程的“锁”_第3张图片
加锁.png

如上图,我们应用了加锁技术后,当线程A访问和修改变量时,会加锁,进行+1操作,此时变量值变为18,然后对其解锁。解锁后,当线程B访问和修改变量时,此时变量的值已经为18,再进行+1操作,完成修改。这样就保证了线程安全。

iOS中的线程同步方案

iOS中为我们提供了一下几种线程同步方案:

  • OSSpinLock —— 自旋锁
  • os_unfair_lock
  • pthread_mutex —— 互斥锁
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • dispatch_semaphore —— 信号量
  • @synchronized

接下来我们逐一对以上方案进行分析:

1、OSSpinLock 自旋锁

OSSpinLock叫做自旋锁,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。目前已经不再安全,可能会出现优先级反转问题。在iOS10版本以后就不再支持这一技术。
如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
需要导入头文件#import

//初始化锁
OSSpinLock lock = OS_SPINLOCK_INIT; 
//尝试加锁(如果需要等待,就不尝试加锁,直接返回false,如果不需要等待就加锁,返回true)
bool result = OSSpinLockTry(&_lock); 
// 加锁
OSSpinLockLock(&_lock);
//解锁
OSSpinLockUnlock(&_lock); 

我们使用自旋锁对上面卖票的例子进行优化:

#import 
@property (assign, nonatomic) OSSpinLock lock;

// 初始化锁
self.lock = OS_SPINLOCK_INIT;

/** 卖1张票 */
- (void)saleTicket {
    // 加锁
    OSSpinLockLock(&_lock);
    
    int oldTicketsCount = self.ticketsCount;
    sleep(.2);
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    
    NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
    
    // 解锁
    OSSpinLockUnlock(&_lock);
}
iOS底层原理探索—多线程的“锁”_第4张图片
加锁后卖票.png

通过打印可以看到,加锁后线程安全,卖票结果也正确。

我们在使用OSSpinLock自旋锁时,系统会报出警告,告诉我们不推荐使用OSSpinLockLock,在iOS 10.0中弃用。并推荐使用中的os_unfair_lock_lock()来代替。

2、os_unfair_lock

os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持。从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
需要导入头文件#import

//初始化
os_unfair_lock moneyLock = OS_UNFAIR_LOCK_INIT; 
// 尝试加锁
os_unfair_lock_trylock(&_ticketLock); 
// 加锁
os_unfair_lock_lock(&_ticketLock);  
// 解锁
os_unfair_lock_unlock(&_ticketLock);  

3、pthread_mutex 互斥锁

mutex叫做互斥锁,等待锁的线程会处于休眠状态
需要导入头文件#import

// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(mutex, &attr);
// 尝试加锁
pthread_mutex_trylock(&_ticketMutex);
// 加锁
pthread_mutex_lock(&_ticketMutex);
// 解锁
pthread_mutex_unlock(&_ticketMutex);
// 销毁属性
pthread_mutexattr_destroy(&attr);

互斥锁的type有以下几种:

#define PTHREAD_MUTEX_NORMAL        0 普通锁
#define PTHREAD_MUTEX_ERRORCHECK    1
#define PTHREAD_MUTEX_RECURSIVE     2  递归锁
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL
pthread_mutex递归锁
// 初始化锁的属性
pthread_mutexattr_t attr;
pthread_attr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);

pthread_mutex条件

// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
// 初始化条件
pthread_cond_t condition;
pthread_cond_init(&condition, NULL);
// 等待条件(进入休眠,放开mutex锁;被唤醒后,会再次对mutex加锁)
pthread_cond_wait(&condition, &mutex);
// 激活一个等待该条件的线程
pthread_cond_signal(&condition);
// 激活所有等待该条件的线程
pthread_cond_broadcast(&condition);
// 销毁相关资源
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&condition);

4、NSLock

NSLock是对mutex普通锁的封装

// 初始化锁
NSLock *lock = [[NSLock alloc] init];
// 尝试加锁
[lock tryLock];
// 指定Date之前尝试加锁
[lock lockBeforeDate:[NSDate date]];
// 加锁
[lock lock];
// 解锁
[lock unlock];

5、NSRecursiveLock

NSRecursiveLock是对mutex 递归锁的封装,APINSLock基本一致。

// 初始化锁
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
// 尝试加锁
[lock tryLock];
// 指定Date之前尝试加锁
[lock lockBeforeDate:[NSDate date]];
// 加锁
[lock lock];
// 解锁
[lock unlock];

6、NSCondition

NSCondition是对mutex和条件的封装

// 初始化锁
NSCondition *condition = [[NSCondition alloc] init];
// 加锁
[condition lock];
// 等待条件
[condition wait];
// Date之前等待条件
[condition waitUntilDate:[NSDate date]];
// 激活一个等待该条件的线程
[condition signal];
// 激活所有等待该条件的线程
[condition broadcast];
// 解锁
[condition unlock];

7、NSConditionLock

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

// 初始化锁
NSConditionLock *condition = [[NSConditionLock alloc] init];
NSConditionLock *condition = [[NSConditionLock alloc] initWithCondition:1];
// 加锁 - 可设置条件值
[condition lockWhenCondition:1 beforeDate:[NSDate date]];
[condition lockBeforeDate:[NSDate date]];
[condition lockWhenCondition:1];
[condition lock];
[condition tryLock];
[condition tryLockWhenCondition:1];
// 解锁
[condition unlockWithCondition:1];
[condition unlock];

8、dispatch_queue(DISPATCH_QUEUE_SERIAL)串行队列

使用GCD的串行队列,实现线程同步

dispatch_queue_t queue = dispatch_queue_create("lock_queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    // 需要执行的任务
});

9、dispatch_semaphore信号量

semaphore叫做信号量,信号量的初始值,可以用来控制线程并发访问的最大数量。信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步。

// 信号量的初始值
int value = 1;
// 初始化信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
// 如果信号量的值 >0,就 -1,然后往下执行代码
// 如果信号量的值 <=0,就会休眠等待,直到信号量的值变成 >0
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 信号量的值 +1
dispatch_semaphore_signal(semaphore);

10、@synchronized

@synchronized是对mutex 递归锁的封装。可以查看源码:objc4中的objc-sync.mm文件。
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作。obj 可以是同一个实例对象,类对象,静态变量

@synchronized(obj) {
    //需要执行的任务
}

iOS线程同步方案性能比较

以上几种锁的性能从高到低排序:

  • os_unfair_lock
  • OSSpinLock
  • dispatch_semaphore
  • pthread_mutex
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSCondition
  • pthread_mutex(recursive)
  • NSRecursiveLock
  • NSConditionLock
  • @synchronized

自旋锁、互斥锁比较

什么情况使用自旋锁比较划算?

  • 预计线程等待锁的时间很短
  • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
  • CPU资源不紧张
  • 多核处理器

什么情况使用互斥锁比较划算?

  • 预计线程等待锁的时间较长
  • 单核处理器
  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈

我们简单的介绍了一下多线程中的线程安全问题,以及解决这些问题的线程同步方案,篇幅有限,实际案例会再后续文章中为大家解读。

更多技术知识请关注公众号
iOS进阶


iOS底层原理探索—多线程的“锁”_第5张图片
iOS进阶.jpg

你可能感兴趣的:(iOS底层原理探索—多线程的“锁”)