1.锁的归类
在OC
中锁分为互斥锁
和自旋锁
两种。
1.自旋锁
是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex
)不同之处在于当自旋锁尝试获取锁时以忙等待
(busy waiting
)的形式不断地循环检查锁是否可用。当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠
),当上一个线程的任务执行完毕,下一个线程会立即执行。
在多CPU
的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。
自旋锁:OSSpinLock(自旋锁)、dispatch_semaphore_t(信号量)
2.互斥锁
当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态
等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务,该任务也不会立刻执行,而是成为可执行状态(就绪
)。互斥锁(mutex
),⽤于保证在任何时刻,都只能有⼀个线程访问该对象。
互斥锁:pthread_mutex(互斥锁)、@synchronized(互斥锁)、NSLock(互斥锁)、NSConditionLock(条件锁)、NSCondition(条件锁)、NSRecursiveLock(递归锁)
3.自旋锁和互斥锁的特点
-
自旋锁会忙等
,所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。 -
互斥锁会休眠
,所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu
可以调度其他线程工作,直到被锁资源释放锁。此时会唤醒休眠线程。 -
自旋锁优缺点
优点
在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU
时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。缺点
在于,自旋锁一直占用CPU
,他在未获得锁的情况下,一直运行自旋,所以占用着CPU
,如果不能在很短的时间内获得锁,这无疑会使CPU
效率降低。自旋锁不能实现递归调用。
4.锁的性能
图中锁的性能从高到底依次是:OSSpinLock(自旋锁)
-> dispatch_semaphone(信号量)
-> pthread_mutex(互斥锁)
-> NSLock(互斥锁)
-> NSCondition(条件锁)
-> pthread_mutex(recursive 互斥递归锁)
-> NSRecursiveLock(递归锁)
-> NSConditionLock(条件锁)
-> synchronized(互斥锁)
2.锁的作用
我们通过一个案例进行分析。模拟一个售票流程,总票数为20
张,有4
个窗口在同时进行售票,实时跟踪剩余票数。见下面代码:
@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 20;
[self testSaleTicket];
}
- (void)testSaleTicket{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 3; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10; i++) {
[self saleTicket];
}
});
}
- (void)saleTicket{
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%lu张",(unsigned long)self.ticketCount);
} else {
NSLog(@"当前车票已售罄");
}
}
@end
复制代码
运行结果如下图:
通过运行结果,发现因为异步操作的原因,出现了数据不安全问题,数据出现了混乱。通常我们会通过加锁的方式来保证数据的安全,用来保证在任一时刻,只能有一个线程访问该对象。
对象上面的案例进行修改,见下图:
添加一个@synchronized
互斥锁,重新运行程序,发现其能够正常运行,并能够保证数据的安全性。@synchronized
用着更方便,可读性更高,也是我们最常用的。
3.@synchronized实现原理
通过上面的案例我们了解到了锁的作用,那么@synchronized
到底做了什么工作呢?这是我们所需要研究分析的。
1.底层探索
-
clang
分析实现原理提供下面一段代码,通过
clang
来查看其底层实现原理,加下图:clang
之后生成.cpp
文件,打卡.cpp
文件,定位到main
函数对应的位置。见下图:可以看到,调用了
objc_sync_enter
方法,并且使用了try-catch
,在正常处理流程中,提供了_SYNC_EXIT
结构体,最后也会调用对应的析构函数objc_sync_exit
。 -
查看汇编流程
首先我们可以通过汇编来分析,底层到底做了哪些操作。通过设置断点,并打开汇编调试,获取以下信息:
通过汇编我们可以发现底层调用了两个方法分别是
objc_sync_enter
和objc_sync_exit
,通过字面可以理解,分别是进入和退出。这与clang
中看到的结果是一样的。
2.实现原理
在libObjc.dylib
源码中分析其实现原理。搜索objc_sync_enter
和objc_sync_exit
两个方法的源码实现:
// 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;
}
// exit
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;
}
复制代码
解读源码发现,enter方法
和exit方法
的实现流程是一一对应的。
首先加锁和解锁都会对obj
进行判断,如果obj
为空,则锁了个寂寞,什么也没有做,在libObjc.dylib
源码中,没有查到objc_sync_nil()
的相关实现。
如果obj
不为空,在enter
方法中,会封装一个SyncData
对象,并对调用mutex
属性进行上锁lock();
在exit
方法时,同样获取对应的SyncData
对象,然后调用data->mutex.tryUnlock();
进行解锁。
-
SyncData
结构分析SyncData
底层定义如下:typedef struct alignas(CacheLineSize) SyncData { struct SyncData* nextData; DisguisedPtr<objc_object> object; int32_t threadCount; // number of THREADS using this block recursive_mutex_t mutex; } SyncData; 复制代码
struct SyncData* nextData;
包含了一个相同的数据结构,说明它是一个单项链表结构object
使用DisguisedPtr
进行了包装threadCount
线程的数量,有多少个线程对该对象进行加锁recursive_mutex_t mutex;
递归锁
从
SyncData
的属性可以判断,@synchronized
支持递归锁,并且支持多线程访问。 -
StripedMap
数据结构首先要分析底层的数据存储结构。
SyncData
存储在一个hash表
中,并且是静态的。见下面代码:static StripedMap<SyncList> sDataLists; class StripedMap { #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR enum { StripeCount = 8 }; #else enum { StripeCount = 64 }; #endif } 复制代码
给表为不同的架构环境提供了不同的容量,真机环境的容量为
8
,模拟环境的容量为64
。而其元素为SyncList
,SyncList
的数据结构为:struct SyncList { SyncData *data; spinlock_t lock; constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { } }; 复制代码
而
SyncData
是一个链表结构,至此形成了一个拉链结构。见下图: -
id2data
方法id2data
实现源码见下图:这里包含
3
个大步骤,首先通过tls
,从线程缓存中获取当前线程的SyncData
进行相关处理;如果缓存中存在对应的SyncData
则从缓存中获取并处理;最后包括一些内部的初始化插入缓存等操作。
流程分支比较多,具体会调用哪些流程呢?下面通过案例结合lldb
调试进行分析。
3.案例跟踪
单线程递归加锁object不变
引入下面的案例,在一个子线程中递归添加同一个锁。见下图:
-
断点
1
:案例的104
行运行程序,在案例的
104
行设置断点,跟踪进入id2data
方法。此时StripedMap
表中64
个数据全是空。见下图:继续跟踪调试,会调用
tls_get_direct
方法,获取当前线程绑定的SyscData
,因为是第一次进行加锁,所以这里的data
是空。见下图:紧接着会从当前线程的缓存列表中获取对应的
SyncData
,很显然此时缓存中也没有存储该对象,所以此时也是空。见下图:当前线程绑定的
SyncData
和线程对应的缓存列表中的SyncData
都为空,则会从哈希表中获取,当前的表中也没有对应的数据,见下图:上面三个地方都没有找到对应的
SyncData
,最终会创建一个SyncData
,并采用头插法将数据插入到对应listp
头部。见下图:完成
SyncData
创建后,会绑定到当前线程上(一个线程只会绑定一个,并且绑定后不再改变),注意此时并没有保存到线程对应的缓存列表中。见下图:最后返回
result
,完成加锁功能。 -
断点
2
:案例的107
行从此断点开始,进行该对象的第二次加锁。进入
id2data
方法,此时哈希表中已经有一个数据,也就是此时对象对应的listp
此时也不再为空(同一个对象)。见下图:继续运行程序,再次获取当前线程绑定的
SyncData
,此时不再为空,并且object
相同。见下图:线程绑定的
SyncData
对应的object
,与此时的object
相同,再次创建锁,并且锁次数++
,见下图: -
断点
3
:案例的110
行进行第三次加锁时,因为此时
object
没有发生改变,线程也没有改变,此时哈希表依然是一个元素,同时对应的listp
也只有一个元素,此时上锁此时会变为3
。见下图:
单线程递归加锁object
变化
引入下面这个案例,我们直接从第二个断点开始分析,见下图:
第一个断点的处理流程我们已经分析了,此时会创建一个新的SyncData
,并且会绑定到当前线程中。
-
断点
2
:案例的108
行object
为person2
,此时线程已经绑定了person1
对应的SyncData
,所以线程绑定关系已经被占用,但是object
不相同。见下图:因为
person2
对象是第一次加锁,所以线程对应缓存列表和listp
中都没有对应的SyncData
。见下图:person2
初次进入,会进行对象的创建,并将SyncData
放入缓存列表中。见下图:如果下次
person2
再次加锁时,会从缓存列表中获取。而如果person1
再次加锁,会从当前线程中获取,因为当前线程已经绑定了person1
对应的SyncData
。
多线程递归加锁object
变化
引入下面的案例,见下图:
上面案例中,前两个加锁过程这里不再分析,和上面单线程是一样的,我们从多线程时开始分析,也就是第113行
开始。
-
断点
1
:案例的113
行从
断点1
处进行跟踪,进入id2data
方法,此时哈希表中的数据个数为2
,也就是外层线程添加的两个SyncData
。见下图:继续跟踪代码,从线程中获取其绑定的
SyncData
,此时为NULL
,因为是新的线程,还没有加过锁,所以绑定数据为空,fastCacheOccupied=NO
。见下图:接着,从缓存列表中获取对应的
SyncData
,也是NULL
,所以这里的缓存列表也是和线程一一对应的。见下图:紧接着,会从
listp
中获取对应的数据,在外层线程中,已经添加了person1
和person2
对应的SyncData
,所以这里是可以获取的。并且会针多线程操作,从而是threadCount
加1
。见下图:获取数据后,因为前面
fastCacheOccupied=NO
,则会将该SyncData
绑定到当前这个线程,也就是每个线程都会默认绑定第一个object
,见下图: -
断点
2
:案例的116
行进行
person2
的加锁操作,此时首先会获取当前线程绑定的SyncData
,这里不相同,因为此时已经绑定了person2
。见下图:然后会从线程对应的缓存列表中获取,因为当前线程没有添加过,所以这里查询不到,最终会在
listp
中获取对应的SyncData
。与此同时会进行threadCount
加1
操作。完成以上操作后,会将该SyncData
添加到线程对应的缓存列表中。见下图:
在新线程中的流程与外层线程的逻辑是一样的,只是线程绑定的数据和缓存列表数据不一样。
4.@synchronized原理总结
通过上面的分析,objc_sync_enter
可以得出以下流程图,在获取SyncData
之后,会调用属性mutex.lock();
进行加锁。见下图:
objc_sync_exit
流程和这个相反,同样会调用id2data
方法,获取SyncData
,对lockCount
和threadCount
进行减操作。如果count
等于0
,则会从相应的绑定关系和缓存列表中移除。
综上:@synchronized
是一个支持多线程的递归锁。