synchronized分析
我们先来看个题目:
- (void)lg_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 < 10; i++) {
[self saleTicket];
}
});
}
- (void)saleTicket{
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%ld张",self.ticketCount);
}else{
NSLog(@"当前车票已售罄");
}
}
然后我们调用上面的方法
self.ticketCount = 20;
[self lg_testSaleTicket];
请问上面的代码设计是否有问题呢?
当然有问题,会存在多个线程操作一个数据ticketCount,导致数据不安全的问题。执行完成后剩余的票数可能不会为0。
既然是多线程导致的数据不安全问题,我们就可以加锁进行解决。
- (void)saleTicket{
// 枷锁 - 线程安全
@synchronized (self) {
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%ld张",self.ticketCount);
}else{
NSLog(@"当前车票已售罄");
}
}
}
我们对卖票的操作部分加上了@synchronized,这样同时只能有一个线程操作ticketCount,从而保证了数据的安全。
下面我们来探究下@synchronized。
appDelegateClassName = NSStringFromClass([AppDelegate class]);
@synchronized (appDelegateClassName) {
}
在synchronized的地方打上断点,然后汇编调试。
image.png
在synchronized的汇编调试代码中,我们看有objc_sync_enter和objc_sync_exit成对的出现。所以这一对函数应该是和synchronized的底层实现相关的。
然后我们就可以通过符号断点,针对objc_sync_enter打个符号断点
objc_sync_enter符号断点
这样我们可以看到objc_sync_enter位于libobjc.A.dylib动态库中,然后我们就可以去open.souce上下载这个源码了。
下载objc源码,然后搜索objc_sync_enter
// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
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;
}
synchronizing是种互斥锁。首先判断objc,如果不存在的话走objc_sync_nil()
,也就是什么都不做。所以在使用@synchronized(obj)进行加锁的时候,如果obj为nil,就是无效的,不会进行加锁。
下面我们看下objc不为空的情况:
构建了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;
里面有个nextData,应该是指向了下一个节点。所以好多这样的节点组成了一个链表似的结构;里面还有个递归锁mutex(递归锁属于互斥锁的一种)。
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
// .......
}
在id2data函数中通过LOCK_FOR_OBJ
函数获取到lockp,LOCK_FOR_OBJ
函数的定义如下
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
static StripedMap<SyncList> sDataLists;
可以看到sDataLists实际上是一个哈希表,表中存在一个个的SyncList对象,SyncList对象的结构中有data和lock。
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
@synchronized底层是封装的互斥锁pThread。
synchronized使用注意点
下面代码可以正常运行吗?
- (void)lg_crash{
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_testArray = [NSMutableArray array];
});
}
}
执行这段代码会导致野指针crash。
GCD里面的_testArray = [NSMutableArray array];
这句代码是在创建新的Array赋值给_testArray,然后释放了旧值。如果此时多个线程同时暂存了旧值,然后就会导致多次释放同一个旧值,从而产生野指针崩溃。
我们可以进行加锁处理。像下面的这样加锁处理可以吗?
- (void)lg_crash{
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (_testArray) {
_testArray = [NSMutableArray array];
}
});
}
}
答案是会产生同样的野指针crash。因为在过程中_testArray可能为空,使用@synchronized锁的对象如果为空的话,相当于不锁。所以会得到同样的crash。此时我们可以将锁的对象_testArray换成self,这样就可以解决问题。但是@synchronized底层需要对哈希表进行处理,过程比较复杂,所以效率低。这里我们可以使用NSLock来进行加锁处理。
- (void)lg_crash{
NSLock *lock = [[NSLock alloc] init];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
_testArray = [NSMutableArray array];
[lock unlock];
});
}
}
NSLock分析
下面的代码可以正常执行吗?
NSLock *lock = [[NSLock alloc] init];
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);
}
[lock unlock];
};
testMethod(10);
})
答案是只打印出一个10,就会卡死。
因为递归调用了testMethod,就会多次进行lock加锁,在一个lock锁定的区域内递归调用再次进行加锁,就会导致堵塞。
因为是递归调用,此时我们应该讲NSLock换成递归锁NSRecursiveLock,就能正常的打印出10 9 8 7 6 5 4 3 2 1
了。
我们在上面代码的最外层再加一个for循环,还可以正常执行吗?
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
for (int i= 0; i<100; 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);
}
[lock unlock];
};
testMethod(10);
});
}
这样就会导致死锁的问题。多个线程进行加锁,互相等待,导致死锁。此时我们只需要将递归锁换成@synchronized就可以解决死锁问题了。因为@synchronized的底层的实现,如果已经锁过一次了就会从缓存中取,而不会再次加锁了。
总结:普通的线程安全可以使用NSLock;如果存在递归调用,使用NSRecursiveLock;如果内部存在递归,外部存在循环或者有其他线程影响,使用@synchronized。
条件锁:NSCondition
调用下面的lg_testConditon
方法,会有问题吗?
- (void)lg_testConditon{
//创建生产-消费者
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self lg_producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self lg_consumer];
});
}
}
- (void)lg_producer{
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
}
- (void)lg_consumer{
while (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
}
上面的代码因为在多线程中,不能保证数据安全。我们需要加锁处理?这里NSCondition就最合适了。使用NSCondition当消费到ticketCount为0的时候,调用wait
等待。当生产一个ticket后,调用signal
发送信号,让等待的可以继续执行。代码实现如下:
- (void)lg_producer{
[_testCondition lock];
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[_testCondition signal];
[_testCondition unlock];
}
- (void)lg_consumer{
// 线程安全
[_testCondition lock];
while (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
// 保证正常流程
[_testCondition wait];
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}
首先锁住生产和消费的代码,然后在消费的时候如果发现ticketCount为0,就wait等待。生产后发送signal,让等待的继续执行消费。
条件锁:NSConditionLock
// 信号量
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1];
NSLog(@"线程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
NSLog(@"线程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
首先创建一个NSConditionLock条件锁,并且设置condition为2。
[conditionLock lockWhenCondition:1];
的意思是如果此时的condition为1,并且没有其他线程获取锁,那么就可以获取锁执行下面的代码。[conditionLock unlockWithCondition:0];
的意思是释放锁,并且将条件置为0。[conditionLock lock];
的意思是不受condition条件的影响。
了解了NSConditionLock后,我们可以知道,线程2肯定是在线程1之前执行。
下面我们来使用汇编来探索一下NSCondition的实现,首先在[conditionLock lockWhenCondition:1];
的地方打上断点,然后开启汇编调试
进入到汇编后我们来到objc_msgSend的地方,这里是调用方法的地方,我们通过lldb命令查看x0和x1的值。可以得到x0是NSConditionLock,x1为lockWhenCondition。也就是我们外面的
[conditionLock lockWhenCondition:1];
这行代码的调用。image.png
我们怎么继续跟踪
[conditionLock lockWhenCondition:1];
这个方法实现呢?此时我们可以通过符号断点的方式,定位到lockWhenCondition
方法的具体执行。添加符号断点-[NSConditionLock lockWhenCondition:]
。然后我们点击继续就会断点在lockWhenCondition
的实现。在lockWhenCondition
的实现汇编代码中又定位到一个objc_msgSend。这里一定是调用了其他的方法。我们打印出方法的执行者和方法名称
image.png
方法的执行者是NSConditionLock,方法名称为lockWhenCondition:beforeDate:
。我们在苹果的官方文档也找到了这个方法。我们继续打符号断点追踪这个方法的实现。
在
lockWhenCondition:beforeDate:
这个方法中定位到一个objc_msgSend。然后打印方法的执行者和方法名,竟然发现是调用了NSCondition的lock方法。也就是说NSConditionLock的底层是通过NSCondition来实现加锁的。然后我们继续往下看,发现有个cmp对比x8和x21,如果相等就跳转
0x18d5cc040
,否则继续往下执行。image.png
打印x8和x21的值,分别为2和1。这个不就是我们在外面使用NSConditionLock设置的条件吗
image.png
继续往下走调用了一个"waitUntilDate:"方法。
网友评论