【iOS开发】—— 初识锁
- 线程安全
- 锁的种类
- 自旋锁
- 定义
- 原理
- 自旋锁缺点
- OSSpinLock(自旋锁)
- 互斥锁
- os_unfair_lock
- pthread_mutex
- NSLock
- NSRecusiveLock
- Semaphore信号量
- @synchronized
- 总结
- 两种之间的区别和联系:
线程安全
当一个线程访问数据的时候,其他的线程不能对其进行访问,直到该线程访问完毕。简单来讲就是在同一时刻,对同一个数据操作的线程只有一个。 而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。 在iOS中, UIKit是绝对线程安全的,因为UIKit都是在主线程操作的,单线程没有线程当然没有线程安全问题,但除此之外,其他都要考虑线程安全问题
iOS解决线程安全的途径其原理大同小异,都是通过锁来使关键代码保证同步执行,从而确保线程安全性,这一点和多线程的异步执行任务是不冲突的。
注: 不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了
下方我们就详细讲解iOS相关锁,本博客采用一个经典的售票例子:
此处展示的是不加锁(即不考虑线程安全)的情况:
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property (nonatomic, assign) NSInteger ticketCount;
@end
#import "ViewController.h"
@interface ViewController ()
@end
int cnt = 0;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.ticketCount = 50;
__weak typeof (self) weakSelf = self;
//一号售卖口
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[weakSelf saleTick];
});
//二号售卖口
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[weakSelf saleTick];
});
}
- (void)saleTick {
while (1) {
if (self.ticketCount > 0) {
self.ticketCount--;
cnt++;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"所有车票已售完,共计售出%d张", cnt);
break;
}
}
}
@end
运行结果的部分截图:
从上图可以发现,输出的顺序是乱序的,而且还显示卖出了54张票。对于上述情况,我们就可以通过加锁来实现修正错误问题。
锁的种类
iOS中的锁有两大类:自旋锁、互斥锁。
自旋锁
定义
自旋锁是一种同步机制,用于在多线程环境中保护共享资源的访问。它通过循环忙等待的方式,而不是阻塞线程,来实现对共享资源的互斥访问。
原理
线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。
自旋锁缺点
- 调用者在未获得锁的情况下,一直运行--自旋,所以占用着CPU资源,如果不能在很短的时间内获得锁,会使CPU效率降低。所以自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下。
- 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁。
OSSpinLock(自旋锁)
OSSpinLock是在libkern库中,使用之前需要引入头文件<libkern/OSAtomic.h>,使用时会出现警告⚠️。
这是因为OSSpinLock存在缺陷,从iOS10开始已经不建议使用了。官方建议使用os_unfair_lock来替代。
下面是使用os_unfair_lock的实例:
// 初始化
spinLock = OS_SPINKLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);
#import "ViewController.h"
#import <os/lock.h>
@interface ViewController ()
@property (nonatomic, assign) os_unfair_lock spinLock;
@end
- (void)saleTick {
while (1) {
// 加锁
OSSpinLockLock(&_spinLock);
if (self.ticketCount > 0) {
self.ticketCount--;
cnt++;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"所有车票已售完,共计售出%d张", cnt);
break;
}
// 解锁
OSSpinLockUnlock(&_spinLock);
}
}
@end
运行结果:
结果就是按照顺序非常规范地卖出了这50张票。
刚才提到了OSSpinLock存在缺陷,其实它的缺陷主要存在两点:
- OSSpinLock不会记录持有它的线程信息,当发生优先级反转的时候,系统找不到低优先级的线程,导致系统可能无法通过提高优先级解决优先级反转问题
- 高优先级线程使用自旋锁忙等待的时候一直在占用CPU时间片,导致低优先级线程拿到时间片的概率降低。
值得注意的是: 自旋锁和优先级反转没有关系,但是正因为有上面两点,所以自旋锁会导致优先级反转问题更难解决,甚至造成更为严重的线程等待问题,所以苹果就废除了OSSpinLock,转而推荐人们使用os_unfair_lock来替代,由于os_unfair_lock是一个互斥锁,所以我们将对其的讲解放到互斥锁中去。
互斥锁
保证在任何时候,都只有一个线程访问对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
互斥锁原理
线程会从sleep(加锁)——> running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销,所以效率是要低于自旋锁的。
互斥锁分为两种: 递归锁、非递归锁
- 递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用。
- 非递归锁:不可重入,必须等锁释放后才能再次获取锁。
os_unfair_lock
上面讲过现在苹果采用os_unfair_lock来代替不安全的OSSpinLock,且由于os_unfair_lock会休眠而不是忙等,所以属于 互斥锁 ,且是非递归互斥锁,下面来看一下它的用法:
os_unfair_lock 在os库中,使用之前需要导入头文件<os/lock.h>
//创建一个锁
os_unfair_lock unfairLock;
//初始化
unfairLock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&unfairLock);
//解锁
os_unfair_lock_unlock(&unfairLock);
实际使用方法:
- (void)saleTick {
while (1) {
//OSSpinLockLock(&_spinklock);
os_unfair_lock_lock(&_lock);
if (self.ticketCount > 0) {
self.ticketCount--;
cnt++;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"所有车票已售完,共计售出%d张", cnt);
os_unfair_lock_unlock(&_lock);
break;
}
os_unfair_lock_unlock(&_lock);
//OSSpinLockUnlock(&_spinklock);
}
}
运行结果:
对于它的定义:
这是对于已经废弃的OSSpinkLock的替换,这个函数不会在争用时旋转,而是在内核中等待被解锁唤醒。与OSSpinLock一样,这个函数并不强制公平或锁排序一例如,解锁程序可能会在唤醒的服务程序获得获得锁的机会之前立即重新获得锁。这可能有利于性能的提高,但也可能导致等待者短缺。不是旋转(忙等),而是休眠,等待被唤醒,所以os_unfair_lock理应是互斥锁。
pthread_mutex
pthread_mutex就是 互斥锁 本身——当锁被占用,而其他线程申请锁时,不是使用忙等,而是阻塞线程并睡眠,另外pthread_mutex也是非递归的锁。
使用时我们需要先引用这个头文件:#import <pthread.h>
具体使用如下:
// 全局声明互斥锁
pthread_mutex_t _lock;
// 初始化互斥锁
pthread_mutex_init(&_lock, NULL);
// 加锁
pthread_mutex_lock(&_lock);
// 这里做需要线程安全操作
// ...
// 解锁
pthread_mutex_unlock(&_lock);
// 释放锁
pthread_mutex_destroy(&_lock);
结果如下:
结果就是按照顺序非常规范地卖出了这50张票。
NSLock
我们的Foundation框架内部也是有一把NSLock锁的,使用起来非常方便,基于互斥锁pthroad_mutex封装而来,是一把互斥非递归锁。
使用如下:
//初始化NSLock
NSLock *lock = [[NSLock alloc] init];
//加锁
[lock lock];
...
//线程安全执行的代码
...
//解锁
[lock unlock];
实际使用(在卖票例子中):
- (void)saleTick {
while (1) {
//OSSpinLockLock(&_spinklock);
//os_unfair_lock_lock(&_lock);
[self.lock lock];
if (self.ticketCount > 0) {
self.ticketCount--;
cnt++;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"所有车票已售完,共计售出%d张", cnt);
//os_unfair_lock_unlock(&_lock);
[self.lock unlock];
break;
}
//OSSpinLockUnlock(&_spinklock);
//os_unfair_lock_unlock(&_lock);
[self.lock unlock];
}
}
运行结果如下:
结果就是按照顺序非常规范地卖出了这50张票。
如果对非递归锁强行使用递归调用,就会在调用时发生线程阻塞,而并非是死锁,第一次加锁之后还没出锁就进行递归调用,第二次加锁就堵塞了线程。
苹果官方文档的描述如下::
可以看到在同一线程上调用两次NSLock的lock方法将会永久锁定线程。同时也重点提醒向NSLock对象发生解锁消息时,必须确保消息时从发送初始锁定消息的同一个线程发送的,否则就会产生未知问题。
非递归互斥锁导致线程阻塞的例子:
- (void)saleTickWithNSLock {
while(1) {
// 加锁
[lock lock];
if (self.ticketSurplusCount > 0) { // 如果还有票,继续售卖
self.ticketSurplusCount--;
cnt++;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { // 如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完,共售出%d张票", cnt);
// 解锁
break;
}
// 解锁
}
}
运行结果如下:
可以看到,因为我们对当前这个线程在执行lock操作后还未unlock的情况下,又进行了NSLock的重复lock加锁操作,所以当前线程发生了阻塞,只进行了一次卖票操作就再不执行其他操作了。
NSRecusiveLock
NSRecursiveLock使用和NSLock类似,不过NSRecursiveLock是递归互斥锁。
//初始化NSLock
NSRecusiveLock *recusiveLock = [[NSRecusiveLock alloc] init];
//加锁
[recusiveLock lock];
...
//线程安全执行的代码
...
//解锁
[recusiveLock unlock];
下面我们举一个NSRecursiveLock递归使用的例子:
#import "ViewController.h"
#import <libkern/OSAtomic.h>
#import <os/lock.h>
@interface ViewController ()
//@property (nonatomic, assign) OSSpinLock spinklock;
//@property (nonatomic, assign) os_unfair_lock lock;
//@property (nonatomic, strong) NSLock* lock;
@property (nonatomic, strong) NSRecursiveLock* recursiveLlock;
@end
int cnt;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.ticketCount = 50;
__weak typeof (self) weakSelf = self;
//self.spinklock = OS_SPINLOCK_INIT;
//self.lock = OS_UNFAIR_LOCK_INIT;
//self.lock = [[NSLock alloc] init];
self.recursiveLlock = [[NSRecursiveLock alloc] init];
// //一号售卖口
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// [weakSelf saleTick];
// });
//
// //二号售卖口
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// [weakSelf saleTick];
// });
for (int i = 0; i < 10; ++i) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[weakSelf saleTick];
});
}
}
- (void)saleTick {
while (1) {
//OSSpinLockLock(&_spinklock);
//os_unfair_lock_lock(&_lock);
//[self.lock lock];
[self.recursiveLlock lock];
if (self.ticketCount > 0) {
self.ticketCount--;
cnt++;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"所有车票已售完,共计售出%d张", cnt);
//os_unfair_lock_unlock(&_lock);
//[self.lock unlock];
[self.recursiveLlock unlock];
break;
}
//OSSpinLockUnlock(&_spinklock);
//os_unfair_lock_unlock(&_lock);
//[self.lock unlock];
[self.recursiveLlock unlock];
}
}
@end
结果如下:
可以看到向同一个线程多次获取递归锁NSRecusiveLock并不会导致程序死锁,而是正常的线程安全地加锁执行。
苹果官方文档的描述如下:
Semaphore信号量
Semaphore信号量也可以解决线程安全问题,GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能:计数小于 0 时需要等待,不可通过。计数为 0 或大于 0 时,不用等待可通过。计数大于 0 且计数减 1 时不用等待,可通过。
Dispatch Semaphore 提供了三个方法:
dispatch_semaphore_create://创建一个 Semaphore 并初始化信号的总量
dispatch_semaphore_signal://发送一个信号,让信号总量加 1
dispatch_semaphore_wait://可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。
注意:
信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量
Dispatch Semaphore 在实际开发中主要用于:
- 保持线程同步,将异步执行任务转换为同步执行任务。
- 保证线程安全,为线程加锁。
@synchronized
@synchronized可能是日常开发中用的比较多的一种递归互斥锁,因为它的使用比较简单,但并不是在任意场景下都能使用@synchronized,且它的性能较低。
使用方法如下:
@synchronized (obj) {}
下面我们来探索一下@synchronized的源码:
- 通过汇编能发现@synchronized就是实现了objc_sync_enter和 objc_sync_exit两个方法。
- 通过符号断点能知道这两个方法都是在objc源码中的。
- 通过clang也能得到一些信息。
#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
{ id _rethrow = 0; id _sync_obj = (id)__null; objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
} _sync_exit(_sync_obj);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_6p_mn3hwpz14_7dg_gr79rtm4n80000gn_T_main_59328a_mi_0);
} catch (id e) {_rethrow = e;}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
} _fin_force_rethow(_rethrow);}
}
}
return 0;
}
总结
两种之间的区别和联系:
1.区别:
- 等待机制:互斥锁是阻塞锁,当锁被其他线程占用时,请求线程会被阻塞;自旋锁是忙等待锁,请求线程会循环忙等待,不断检查锁的状态。
- CPU占用:自旋锁是忙等待,当线程持有自旋锁时间较长时,其他等待线程会一直忙等待,浪费CPU资源;互斥锁是阻塞,当线程请求锁时,会被阻塞,释放CPU资源给其他线程。
- 适用场景:自旋锁适用于多核心CPU、共享资源占用时间较短的情况;互斥锁适用于共享资源占用时间较长的情况。
2.联系:
- 保护共享资源:自旋锁和互斥锁都用于保护共享资源,确保多线程环境下对共享资源的访问安全。
- 互斥性质:自旋锁和互斥锁都是互斥的,同一时间只能有一个线程持有锁,其他线程必须等待。
- 锁的操作:自旋锁和互斥锁都具有获取锁和释放锁的操作,线程在获取锁后可以访问共享资源,完成操作后释放锁,让其他线程获取锁。