iOS--OC底层原理文章汇总
本文探索常用锁以及@synchronized
底层的原理。
锁的分类
在开发中,使用最常见的恐怕就是@synchronized
(互斥锁)、NSLock
(互斥锁)、以及dispatch_semaphore
(信号量)。其实还有许多种,总分类有:互斥锁、自旋锁,细分之下多出了: 读写锁、递归锁、条件锁、信号量,后三者是对基本锁的上层封装。先介绍几个概念。
【自旋锁】是用于多线程同步的一种锁,线程反复检查锁变量是否可用(即可重入特性)。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
【互斥锁】是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
递归锁(recursive_mutext_t)是一种特殊的互斥锁。
【读写锁】是计算机程序的并发控制的一种同步机制(也称“共享-互斥锁”、多读-单写锁) 用于解决多线程对公共资源读写问题。读的操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。
【信号量】是一种更高级的同步机制,互斥锁可以说是semaphore
在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
【条件锁】:条件锁就是条件变量,当进程的某些资源要求不满足时就进入休眠,即锁住了,当资源被分配到了,条件锁打开了,进程继续运行。
对应有以下锁:
- OSSpinLock(自旋锁)
- dispatch_semaphone(信号量)
- pthread_mutex(互斥锁)
- NSLock(互斥锁)
- NSCondition(条件锁)
- os_unfair_lock (互斥锁)
- pthread_mutex(recursive 互斥递归锁)
- NSRecursiveLock(递归锁)
- NSConditionLock(条件锁)
- synchronized(互斥递归锁)
OSSpinLock(自旋锁)
- 与互斥锁(阻塞-睡眠)不同,自旋锁加锁后是进入忙等状态。
- 如果共享数据已经有其他线程加锁了,线程会以忙等的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。
OSSpinLock效率很高,但是已不再安全。如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。
ibireme 大神--<不再安全的 OSSpinLock>
所以苹果已经推荐使用os_unfair_lock
。
- os_unfair_lock基本使用
关于os_unfair_lock
是苹果在iOS10之后推出,它属于互斥锁,os_unfair_lock
加锁会让等待的线程进入休眠状态,而不是忙等。这样就提高了安全也降低了性能损耗。
#import
// 创建一个 os_unfair_lock_t 锁
os_unfair_lock_t unfairLock;
// 先分配此类型的变量并将其初始化为OS_UNFAIR_LOCK_INIT
unfairLock = &(OS_UNFAIR_LOCK_INIT);
// 尝试加锁,返回YES or NO
os_unfair_lock_trylock(unfairLock)
// 加锁
os_unfair_lock_lock(unfairLock);
// 解锁
os_unfair_lock_unlock(unfairLock);
dispatch_semaphone(信号量)
信号量适用于异步线程同步操作的场景。
// 创建使用
dispatch_semaphore_create(long value); // 创建信号量
dispatch_semaphore_signal(dispatch_semaphore_t deem); // 发送信号量
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); // 等待信号量
// 注意: 发送信号量和信号等待是成对出现
// 常见使用场景
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{ // ①
NSLog(@"任务1:%@",[NSThread currentThread]);
dispatch_semaphore_signal(sem); // ③
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // ②
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务2:%@",[NSThread currentThread]);
dispatch_semaphore_signal(sem); // ⑤
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // ④
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务3:%@",[NSThread currentThread]); // ⑥
});
// 执行顺序:① - ② - ③ - ④ - ⑤ - ⑥
}
通过控制信号量通过数,就可实现锁的功能。
pthread_mutex(互斥锁)
- 阻塞线程并
sleep
(加锁),加锁过程中切换上下(主动出让时间片,线程休眠,等待下一次唤醒)、cpu的抢占、信号的发送等开销。 - 如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
- 互斥锁范围,应该尽量小;锁定范围越大,效率越差。
- 能够给任意NSObject对象加锁。
加解锁流程:sleep(加锁) -> 出让时间片 -> 线程休眠 -> 等待唤醒 -> running(解锁)
时间⽚(quantum):系统给每个正在运行的进程或线程微观上的一段CPU时间。
// 导入互斥锁头文件--C语言
#import
// 可添加成员变量
pthread_mutex_t mutex;
- (void)myfun
{
pthread_mutex_init(&mutex, NULL);
}
- (void)MyLockingFunction
{
pthread_mutex_lock(&mutex);
// Do something.
pthread_mutex_unlock(&mutex);
}
- (void)dealloc
{
// 不用要释放掉
pthread_mutex_destroy(&mutex);
}
// 这只是简单使用,具体还需针对进行错误代码处理
互斥锁 vs 自旋锁
相同:都能保证同一时间只有一个线程访问共享资源。都能保证线程安全。
不同:
- 互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
- 自旋锁:如果共享数据已经有其他线程加锁了,线程会以忙等的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。
NSLock(互斥锁)
NSLock是对底层pthread_mutex
的封装。一般使用有:
self.lock = [[NSLock alloc] init];
[self.lock tryLock]; // 尝试加锁;返回YES or NO
[self.lock lock]; // 加锁
[self.lock unlock]; // 解锁
底层原理
NSLock
是Foundation
下的,闭源则源码不可见。借助Swift的Foundation
可以看看同集成NSLocking
的NSLock
在底层做了什么操作。
-
- 调用必须初始化;而底层则直接调用了互斥锁
pthread_mutex_init
.(可以知道性能相近的原因了)
- 调用必须初始化;而底层则直接调用了互斥锁
-
- 底层实现也是调用了
pthread_mutex
的lock
和unlock
.即就是对pthread_mutex
的封装。
- 底层实现也是调用了
在Apple官方文档中指出
Warning
The NSLock class uses POSIX threads to implement its locking behavior. When sending an
unlock message to an NSLock object, you must be sure that message is sent from the
same thread that sent the initial lock message. Unlocking a lock from a different thread can result in undefined behavior.
Tra:本NSLock类使用POSIX线程执行其锁定行为。向NSLock对象发送解锁消息时,必须确保该消
息是从发送初始锁定消息的同一线程发送的。从其他线程解锁锁可能导致未定义的行为。
所有它仅限用于同一线程中,且也不应使用此类来实现递归锁。lock
在同一线程上两次调用该方法将永久锁定您的线程。原因是加锁还未解锁又再一次加锁,一直在加锁就会陷入死锁状态。如下:
NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<50; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
[lock unlock];
});
}
可以使用NSRecursiveLock
来实现递归锁,也可使用@synchronized
替代处理。
@synchronized
@synchronized
是开发中用的非常广泛的一种锁,目的就是防止不同的线程同时执行同一段代码。但是就其性能而言,可谓惨不忍睹。常言存在即合理,广泛使用,面试中常常被提及,就需要探索一下其底层原理。
首先确定下研究方法:1. 汇编;2. Clang。
有这样一个例子,「onePx」奶茶店生意很好,奶茶就剩20杯的量,三个窗口卖(类似三条条线程),这还有越卖越多情况,就不符合逾期了。这就多线程对同一资源访问,发生了数据错乱。
当然了,主题是锁,就通过加锁即可解决。譬如加一个
@synchronized
,就完美控制。
汇编
在@synchronized
打下一个端点,打开汇编,Xcode菜单栏,Debug -> Debug Workflow -> Always Show Disassembly
通过汇编可以窥见一二,在加锁和无锁的情况下有很大区别,执行流程变得更加复杂,且多出两个关键方法:objc_sync_enter、objc_sync_exit
,这就是@synchronized
的进出口方法。
Clang
在main.m中编写一个@synchronized
方法
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
@synchronized (appDelegateClassName) {
}
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
然后对其Clang指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
编译出已C++文件,查看底层
由图中可验证汇编形式下,
@synchronized
的底层会调用objc_sync_enter、objc_sync_exit
。如果捕获到异常,就把异常抛出。
objc_sync_enter
在之前的工程中下一个方法为objc_sync_enter
的符号断点,我们可以知道底层源码是在libobjc.A.dylib
中。也可以在@synchronized
处下断点,进入汇编然后调试到objc_sync_enter
,点击底部调试菜单栏step into
进入objc_sync_enter
的深一层汇编,也可以知道其归属于libobjc.A.dylib
范畴。
其实从这里就可以知道它在底层大致做了什么,做一个等值判断,根据判断结果,它会调用
id2Data
的一个方法,然后再会调用一个os_unfair_recursive_lock_lock_with_options
;否则就跳转调试超父类,调用一个方法objc_sync_nil
。严谨一点还是要走底层代码摸索一波。
打开一份前面文章分析用过的objc源码,我们可以查找到对应的源码
// Begin synchronizing on 'obj'. 开始同步
// Allocates recursive mutex associated with 'obj' if needed.
// 如果需要,分配与“ obj”关联的递归互斥体。 这里可以知道,它是一把递归互斥锁,具体看底层。
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
// 成功时返回一个 OBJC_SYNC_SUCCESS
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
// 先判空
if (obj) {
// id2data -> 关键方法。obj 不为空,从id2Data 获取一个SyncData类型数据,然后加锁。
SyncData* data = id2data(obj, ACQUIRE);
ASSERT(data);
// 加锁
/**
mutex 类型为 recursive_mutex_t;
*/
data->mutex.lock();
} else {
// @synchronized(nil) does nothing 如果加锁传入的obj为空,什么也不做
// 如果obj 为空,报以奔溃 objc_sync_nil()
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
- SyncData结构 + SyncList结构
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData; // SyncData -> SyncData ,不断的链接。这证明是一个链表结构
DisguisedPtr object; //通过运算使指针隐藏于系统工具,同时保持指针的能力,其作用是通过计算把保存的 T 的指针隐藏起来,实现指针到整数的映射。
int32_t threadCount; // number of THREADS using this block。该代码块的线程数
recursive_mutex_t mutex; // 证明其底部就是递归互斥
} SyncData;
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
// Use multiple parallel lists to decrease contention among unrelated objects.使用多个并行列表以减少不相关对象之间的争用。
// 哈希出对象的数组下标,然后取出数组对应元素的 lock 或 data
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap sDataLists;
SyncData
做为一个一个的节点,依次存储,每个 SyncList 结构体都有个指向 SyncData
节点链表头部的指针,也有一个用于防止多个线程对此列表做并发修改的锁。类似SyncData -> SyncData -> SyncData ...
,它是链表形式存储,不同的并行列表间互相不对数据进行访问。
链表的底层是通过哈希算法来存储的,即StripedMap
底层就是是将对象指针在内存的地址转化为无符号整型,通过算法((addr >> 4) ^ (addr >> 9)) % StripeCount
, 来获取下标indexForPointer
。
SyncList示意图
-
obj == nil时啥也不做
无论是objc_sync_enter
还是objc_sync_exit
都调用了一个关键方法id2Data()
,则我们的侧重点就是在里面的实现。
id2Data
- 第一个情况 : SUPPORT_DIRECT_THREAD_KEYS = 1;即为快速缓存查找
- 在线程缓存池中进行快速查找对象,获取锁的数量,再做一些异常判断;
- 判断
why
,如果类型为ACQUIRE
,则使得lockCount
加1,再存储下来;
如果类型为RELEASE
,则使得lockCount
减1,再存储下来;
如果类型为CHECK
,啥也不做。
- 第二个情况:SyncCache缓存查找
SyncCache结构,与tls线程缓存相似,它也是链式存储。
typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block 线程加锁次数
} SyncCacheItem;
typedef struct SyncCache {
unsigned int allocated;
unsigned int used;
SyncCacheItem list[0];
} SyncCache;
有所不同的是,它如果有多条线程时,就会有多个这样的list。
这个情况下就是在SyncCache的缓存列表里,不同线程间,查找是否有匹配对象,如果找到匹配对应的类型,进行lockCount
加和减操作。
- 第一次进来
先在线程池中遍历获取p->nextData
,通过p =*listp
取出第一个SyncData
数据, 判断p->object
是否为传入object
, 如果相等,则存储下object
的持有者SyncData
的p
;跳转到done
,通过判断fastCacheOccupied
, YES时存储到快速线程缓存中;否则存储到线程缓存(SyncCache)中。
【总结】
- 首次进来: 没有锁,
threadCount = 1,lockCount = 1
, 存到tls_set_direct
中; - 不是第一次进来,是在
tls链表
进行快速缓存查找的,它们是在同一个线程进行lockCount
加,并且将result
存到tls_set_direct
; - 不是第一次但是在SyncCache中查找的,则可能在一个线程或多个线程中遍历SyncCacheItem类型的list单向链表,查找SyncData,查找到也会对
lockCount
加操作。
objc_sync_exit
// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
}
return result;
}
这解锁就是反向操作了,这可不是像某些人反向吸Y那样子。
它要判断obj
,然后在id2Data
里面做release操作,使得lockCount减少;
如果拿到的非空data闲尝试解锁,解锁成功的即返回result;解锁失败的,及时报错。
【@synchnized 坑点】
坑点1:经过以上的分析,@synchnized
慢的原因就非常清楚了,它的加锁解锁都经过了一些列的增删改查再加缓存,链表接口的存取都会影响速度。
坑点2:也是一个面试题
- (void)testSynchronized {
for (int i = 0; i < 10000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.dataSources = [NSMutableArray array];
});
}
}
执行之后会卡死。可以通过开启
再次运行就可以看到奔溃。原因是在反复初始化,调用
setter
retain 新值,释放旧值。线程不断的release旧值,导致了野指针。
尝试锁一下
- (void)testSynchronized {
for (int i = 0; i < 10000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self.dataSources) {
self.dataSources = [NSMutableArray array];
}
});
}
}
但是会出现同样的奔溃,原因是:self.dataSources
的生命周期,它是会被释放的,释放后为nil
,在前面分析中就可以知道,如果加锁对象为nil,则在「锁」内就do nothing
。
所以@synchnized
一般锁self,保证锁住的objc的生命周期未结束。但亦不能一直锁self
,在底层objc_sync_enter
的时候,self
的链表会很多,就会导致链表查询很繁琐,性能降低更加明显。
补充:atomic & nonatomic
atomic
- atomic 原⼦属性(线程安全),针对多线程设计的,需要消耗⼤量的资源
- atomic 本身就有⼀把锁(⾃旋锁)
- 保证同⼀时间只有⼀个线程能够写⼊,但是同⼀个时间多个线程都可以取值。(单写多读:单个线程写⼊,多个线程可以读取)
nonatomic
- nonatomic ⾮原⼦属性
- nonatomic:⾮线程安全,适合内存⼩的移动设备。
属性应都声明为 nonatomic
;
尽量避免多线程抢夺同⼀块资源;
尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减⼩移动客户端的压⼒。