这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战
@synchronized
上篇我们分析了@synchronized的结构,那么SyncData是怎么创建的呢?不同的对象或者不同的线程又是怎么处理的呢?我们知道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 = result;
问题是什么情况下才会进入到这里。我们可以代码验证下,在上节介绍到,模拟器的情况下,这个StripeMap有64位,这里为了增加哈希冲突概率我们修改为1.
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 1 };
#endif
}
代码验证的步骤,首先在libObjc源码工程下写入这行代码,并在SyncData初始化时下断点
Person *p = [Person alloc] ;
Person *p1 = [Person alloc];
for (int i = 0; i < 10; i++) {
NSLog(@"---");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (p) {
NSLog(@"Person --- %@, %d", [NSThread currentThread], i);
@synchronized (p1) {
NSLog(@"Person --- %@, %d", [NSThread currentThread], i);
}
}
});
}
配合着调用栈的线程来观察,因为是开了多条线程
我们发现在初次进来的时候这个
data 并没有值,在240行走完之后,再p以下
这个就是创建的第一个
SyncData,我们看下第二个创建的时候,有没有冲突, 等到第二次的点过来的时候
我们知道拉链法是采用的头插法插入的
这里
result->nextData刚好指向了第一个SyncData。在同一个线程空间里,对象不是生成一整个拉链,当产生哈希冲突的时候就会有拉链的生成。上面的例子只有一个SyncList的时候,p1和p2都会在【0】这条拉链里面,每一个SyncData里面都会有一个标记threadCount来表明被多少条线程锁。
锁的归类
⾃旋锁:线程反复检查锁变量是否可⽤。由于线程在这⼀过程中保持执⾏,因此是⼀种忙等待。⼀旦获取了⾃旋锁,线程会⼀直保持该锁,直⾄显式释放⾃旋锁。⾃旋锁避免了进程上下⽂的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。有互斥和同步的概念,多条线程同时处理的时候,按照响应的顺序来处理。
互斥锁:是⼀种⽤于多线程编程中,防⽌两条线程同时对同⼀公共资源(⽐如全局变量)进⾏读写的机制。该⽬的通过将代码切⽚成⼀个⼀个的临界区⽽达成
互斥锁有:NSLock pthread_mutex @synchronized
在PosixThread中定义有⼀套专⻔⽤于线程同步的mutex函数,⽤于保证在任何时刻,都只能有⼀个线程访问该对象。当获取锁操作失败时,线程会进⼊睡眠,等待锁释放时被唤醒
1.创建和销毁
- A:POSIX定义了⼀个宏
PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁 - B:
intpthread_mutex_init(pthread_mutex_t*mutex,constpthread_mutexattr_t*mutexattr) - C:
pthread_mutex_destroy()⽤于注销⼀个互斥锁
2.锁操作
intpthread_mutex_lock(pthread_mutex_t*mutex)intpthread_mutex_unlock(pthread_mutex_t*mutex)intpthread_mutex_trylock(pthread_mutex_t*mutex)pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY⽽不是挂起等待
NSLock
- (void)test1 {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
});
}
如果我们此时在外层嵌套一个for循环呢
- (void)test1 {
for (int i= 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
});
}
}
我们可以发现,此时数据就出现了问题。此时我们就需要加一把锁,此时打印输出就没有问题了。
[lock lock];
testMethod(10);
[lock unlock];
如果我们换个地方加锁呢?
此时在47行加锁的时候,还没有
unlock解锁的时候就再次调用testMethod,造成了一直加锁一直加锁成了死锁。从这个方向也验证了当前的NSLock这把锁不可递归。
NSRecursiveLock
我们在上面的lock和unlock处使用NSRecurisiveLock来加锁
也会崩溃也只打印了一遍,我这里明明外层还有个循环,所以说
NSRecursiveLock可递归但是不是多线程的。只是解决了递归性,如果使用@synchronized则完美的解决了这些问题。
NSCondition
NSCondition的对象实际上作为⼀个锁和⼀个线程检查器:锁主要为了当检测条件时保护数据源,执⾏条件引发的任务;线程检查器主要是根据条件决定是否继续运⾏线程,即线程是否被阻塞
- 1:
[condition lock]// ⼀般⽤于多线程同时访问、修改同⼀个数据源,保证在同⼀时间内数据源只被访问、修改⼀次,其他线程的命令需要在lock外等待,只到unlock,才可访问 - 2:
[condition unlock];// 与lock同时使⽤ - 3:
[condition wait];// 让当前线程处于等待状态 - 4:
[condition signal];// CPU发信号告诉线程不⽤在等待,可以继续执⾏
比较典型的是生产消费者模式。可以多线程生产,但是消费的时候必须要保证生产数量 > 0,如果消费的时候库存等于0 ,就等待。
- (void)producer{
[_testCondition lock]; // 操作的多线程影响
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[_testCondition signal]; // 信号
[_testCondition unlock];
}
- (void)consumer{
[_testCondition lock]; // 操作的多线程影响
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
[_testCondition wait];
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}
NSLock、NSRecursiveLock、NSCondition都来源于底层Foundation框架。
Foundation源码关于锁的封装
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}
实际上这个锁是一个协议,遵守NSLocking协议的锁都要实现lock和unlock两个方法。在Foundation源码中NSLock的初始化调用的是pthread_mutex_init方法
NSLock的相关定义:
open class NSLock: NSObject, NSLocking {
public override init() {
pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
}
open func lock() {
pthread_mutex_lock(mutex)
}
}
NSRecursiveLock的相关定义:
open class NSRecursiveLock: NSObject, NSLocking {
public override init() {
super.init()
#if CYGWIN
var attrib : pthread_mutexattr_t? = nil
#else
var attrib = pthread_mutexattr_t()
#endif
withUnsafeMutablePointer(to: &attrib) { attrs in
pthread_mutexattr_init(attrs)
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))// 递归
pthread_mutex_init(mutex, attrs)
}
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
}
open func lock() {
pthread_mutex_lock(mutex)
}
}
经过两个对比,可以发现递归锁NSRecursiveLock多了几行代码PTHREAD_MUTEX_RECURSIVE这里就是设置可递归的地方。
NSConditionLock
- (void)testConditonLock{
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];
sleep(0.1);
NSLog(@"线程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
}
执行顺序:这里的执行顺序一定是2比1先执行。
疑问:
- 1:
NSConditionLockVSNSCondition - 2: 2 -> 是什么东西
- 3:
lockWhenCondition-> 如何控制 - 4:
unlockWithCondition又做了什么?
-[NSConditionLock initWithCondition:]分析
带着上面那几个疑问,我们使用真机定位一下源码看看
这里也再次验证了上面提到了定位到Foundation框架的原因。真机的话,我们知道x0,x1和x2是参数
我们在汇编里面主要看
bl跳转相关的一些处理,我们把所有的bl都断点一下行数分别在21,23,30,35,37,52
可以得出有个对象调用了
[init]带了一个参数2,依次循环LLDB输出,大致得出方法调用顺序:
-[NSConditionLock initWithCondition:]
[? init: 2][NSConditionLock init: 2][NSConditionLock zone][NSCondition allocWithZone][NSCondtion init]
接着继续就走到了52行的retain方法,我们知道返回值一般放在x0里面,所以我们输出一下
而上面已经分析出来的方法可以猜测到,前面5步已经开辟了内存空间,那么此时我们可以使用x/4gx分析,得出结论,在
NSConditionLock内部有一个成员变量NSCondition,外部传进来的值也保存进来了。
-[NSConditionLock lockWhenCondition:]分析
此时在这里的
12,24行也加上断点,跟下跳转的流程,慢慢调试,得出下面的方法。
-[NSConditionLock lockWhenCondition:]
[NSDate distantFuture][NSConditionLock lockWhenCondition:beforeDate:][NSCondition lock][NSCondition waitUntilData]此时我们可以在方法[NSConditionLock lockWhenCondition:beforeDate:]加一个符号断点,跟下这里面的方法。最终得到了以上的步骤,用相同的方法跟踪下[NSConditionLock lockWhenCondition:]的流程
-[NSConditionLock lockWhenCondition:]
[NSCondition lock][NSCondition broadcast][NSCondition unlock]
最直观的方法,我们可以直接打开Foundation的源码,但是上面的流程也不是没有用处,万一源码不公开呢?上面的是分析的常规思路。