探索底层原理,积累从点滴做起。大家好,我是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];
}
});
}
我们打印一下执行结果:
我们发现,经过15次卖票后,正常来说15张票已经全部卖完才对,但是最后一次打印还剩5张票。这就出现了线程安全问题。
我们再来看下面一个例子:
例如上图中,一个
integer
类型的对象值为17,当线程A和线程B同时访问到的时候,线程A做+1操作,同时线程B也做+1操作。
由于两个线程访问到integer
类型对象时的值都为17,分别做+1操作后变量的值变为18。但是实际的结果应为做了两次+1操作,值应该为19。这就出现了问题。
那么多线程同样提供了解决方案:使用线程同步技术。
线程同步技术
常见的线程同步技术是加锁
我们再讲上面的例子优化:
如上图,我们应用了加锁技术后,当线程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 自旋锁
叫做自旋锁,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。目前已经不再安全,可能会出现优先级反转问题。在iOS10版本以后就不再支持这一技术。OSSpinLock
如果等待锁的线程优先级较高,它会一直占用着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);
}
通过打印可以看到,加锁后线程安全,卖票结果也正确。
我们在使用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 递归锁
的封装,API
跟NSLock
基本一致。
// 初始化锁
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进阶