前言
锁对我们而言不陌生但是又很陌生,当多个线程操作同一个资源的时候为了内存安全,婚恋相亲系统开发需要对资源进行保护,那么我们需要使用锁。常用的锁如@synchronized
、NSRecursiveLock
、NSLock
、以及属性的原子操作锁atomic
等等,他们有什么区别呢?我们该如何理解并正确使用呢?
疑问?
看一下下面的例子
- (void)viewDidLoad {
[super viewDidLoad];
//假设有100张电影票
self.ticketCount = 100;
//线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 60; i++) {
[self saleTicket];
}
});
//线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 50; i++) {
[self saleTicket];
}
});
}
- (void)saleTicket{
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%lu张",(unsigned long)self.ticketCount);
}else{
NSLog(@"当前车票已售罄");
}
}
输出:
分析:如上图所示,如果婚恋相亲系统开发不加锁,输出当前余票就会发生错乱,我们想看到的应该是顺序递减的,那么在上面saleTicket
方法中加一个@synchronized
锁就可以解决这种多线程导致的数据不安全的问题。
- (void)saleTicket{
@synchronized (self) {
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%lu张",(unsigned long)self.ticketCount);
}else{
NSLog(@"当前车票已售罄");
}
}
}
那么再看下面的例子
//初始化NSLock锁
self.mylock=[[NSLock alloc]init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
//block处理业务代码
[self.mylock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[self.mylock unlock];
};
testMethod(10);
});
输出:
**current value = 10**
分析:testMethod是一个处理业务的代码块,里面进行了递归调用,如果使用NSLock锁就会产生死锁,因为理论上输出日志应该是从10开始递减,那么换成@synchronized
如何呢?
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
@synchronized (self) {
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
}
};
testMethod(10);
});
输出:
current value = 10
current value = 9
current value = 8
current value = 7
current value = 6
current value = 5
current value = 4
current value = 3
current value = 2
current value = 1
分析:看到这我们至少知道@synchronized可以解决NSLock因为递归导致的死锁问题,说明NSLock是一把非递归锁,如果把NSLock换成递归锁NSRecursiveLock
也能解决这个问题,那么NSRecursiveLock能解决多线程
的问题吗?
self.recursiveLock = [[NSRecursiveLock alloc] init];
for (int i= 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[self.recursiveLock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[self.recursiveLock unlock];
};
testMethod(10);
});
}
结果:在婚恋相亲系统开发多线程的情况下NSRecursiveLock递归锁会导致程序奔溃,而换成@synchronized同样可以解决在多线程下的递归问题。看样子@synchronized
确实有点屌,不仅能多线程加锁而且可以递归调用,下面就分析下源码看看它是如何实现的。
@synchronized
随便写一个@synchronized通过汇编看看底层调用了什么符号,然后再跟进源码看细节,也可以通过clang编译一下源文件看看@synchronized编译成了什么。
通过汇编发现@synchronized底层是调用了objc_sync_enter
加锁和objc_sync_exit
解锁,我们进objc源码看下这两个函数。
objc_sync_enter、objc_sync_exit
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;
}
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;
}
分析:objc_sync_enter、objc_sync_exit逻辑上是一样的,一个是加锁一个是解锁。首先判断obj是否为空,如果不为空就构造一个SyncData对象并且加锁,如果为空就什么也不操作,所以在使用@synchronized(obj)
时必须要传递一个对象否则加不了锁。加锁操作是通过对象SyncData获取的mutex.lock()
,看下SyncData
结构以及id2data
方法是如何生成的SyncData。
SyncData
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;//单向链表结构 指向下一个syncdata
DisguisedPtr<objc_object> object;//把传入进来的obj构造成一个统一的结构
int32_t threadCount; //操作锁的线程数
recursive_mutex_t mutex;//递归锁
} SyncData;
分析:通过SyncData
对象我们稍微有了一点点明悟,threadCount
应该就是@synchronized可以多线程加锁的原因,recursive_mutex_t
应该就是@synchronized可以递归的原因,至于nextData
我们目前只知道是一个链表结构,接下来看下创建这个对象的函数id2data()
static SyncData* id2data(id object, enum usage why)
{
//从哈希表SyncList中获取object对象的锁,目的是保证该方法代码块的内存安全
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
//从哈希表SyncList中通过object对象的哈希下标获取syncData地址
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS
bool fastCacheOccupied = NO;
//1.从线程局部存储中查找根据object对应的SyncData,tls为线程局部存储,每个线程都有唯一一个
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
//同一个对象单线程递归加锁,加锁就lockcount++,解锁就lockCount--,
if (data->object == object) {
//....
}
}
//2.从线程缓存中查找SyncData,如果是加锁lockcount++,解锁就lockCount-
SyncCache *cache = fetch_cache(NO);
if (cache) {
//...
}
lockp->lock();
//3.多线程进入的流程,进行threadcount++
{
//省略....
}
//4.创建SyncData,线程默认为1,nextData指定为上一个SyncData,头插法
//第一次加锁或者单线程不同对象加锁会进入
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;
//通过*listp给哈希表赋值
*listp = result;
done:
lockp->unlock();
if (result) {
//同一线程,对象第一次加锁,tls中没有,设置tls
if (!fastCacheOccupied) {
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
{//同一线程,其他对象保存在线程缓存,tls内存有限
// Save in thread cache
if (!cache) cache = fetch_cache(YES);
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
}
return result;
}
分析:创建SyncData流程如下
- 从
哈希表
SyncList中通过object对象的哈希下标获取listp,注意是通过对象的哈希下标寻址,在存储的时候如果对象的哈希下标一样
,就通过拉链法
存储。 - 从
tls
(线程局部存储
)中获取object对应的syncData,如果获取到了,就把该对象在tls中lockcount++
或者lockcount--
,加还是减根据是加锁还是解锁判断。每个对象
都绑定一个syncData
,每个线程的tls都不一样,如果是同一个对象单线程递归加锁,就会进入该流程。 - 从
线程缓存
中获取object对应的syncData,如果获取到了,就把该对象在线程缓存中lockcount++或者lockcount--,加还是减根据是加锁还是解锁判断。tls内存容量很小,一般同一个线程第一个对象会存在tls中,其他对象都会存在线程缓存中。 - 从
tls
和线程缓存都找不到该对象,说明是不同线程或不同对象又或者是第一次加锁,如果listp不为空,就循环遍历listp的拉链表
,直到找到与object对应的syncData,使它threadcount++
,简单点说就是多线程对同一个对象加锁
时会进入该流程。 - 从
tls和线程缓存
都找不到该对象并且非多线程,根据object创建syncData,并且指定syncData中属性nextData
为上一个syncData,这是头插法
,也就是每次新来的数据会插在之前数据的前边 - 同一线程第一次加锁会把syncData插入到tls,同一线程第二次加锁会把syncData插入到线程缓存。
哈希表SyncList
源码如下:
static StripedMap<SyncList> sDataLists;
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
// ......
}
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
static sDataLists
是一个全局的哈希表
,真机情况的存储SyncList
个数是8
个,其它环境64
个,SyncList是一个封装着SyncData的结构体,使用拉链法来存储 SyncData,同一个线程不同对象加锁时很大概率会触发拉链存储,拉链存储
的前提是对象的哈希值一样
,用下面这张图辅助理解下这个哈希表的结构
lldb断点调试演示拉链
为了方便断点调试演示拉链,把源码中StripedMap里StripeCount改为2增加hash冲突
的概率。
同一线程不同对象加锁
LGTeacher* lgter1=[[LGTeacher alloc]init];
LGTeacher* lgter2=[[LGTeacher alloc]init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (lgter1) {
NSLog(@"对象1加锁");
@synchronized (lgter2) {
NSLog(@"对象2加锁");
}
}
});
断点打在第一个@synchronized
对象lgter1
加锁,此时*listp
值为空,创建syncData对象result并且把result
的nextData指向*listp
即为空对象。过了241
行后,*listp被赋值成了对象result,此时哈希表被更新有值了,继续往下走会把对象lgter1对应的syncData
存进tls
断点打在第二个@synchronized
对象lgter2
加锁时,此时*listp
不会空,说明lgter2和lgter1哈希下标是一样的
,listp是根据下标在哈希表中取值的,继续往下走不会进tls,因为虽然是同一个线程但不是同一个对象,也不会进线程缓存中查找,最后会创建一个新的syncData对象result并且把result的nextData指向listp即为上一个对象lgter1的syncData,这样就形成了拉链
。
婚恋相亲系统开发继续往下走对象会被存进线程缓存
而不是tls
,tls只有线程第一次给对象加锁时才会被存入。
声明:本文由云豹科技转发自顶风尿一丈博客,如有侵权请联系作者删除