iOS 中的锁(3)
不想篇幅太长,再开一篇继续探究iOS中的锁。
注:本文主要通过Objective-C
语言进行体现,其实跟Swift
也差不多。
本文介绍了iOS中的NSRecursiveLock、atomic两种锁。
1. NSRecursiveLock
NSRecursiveLock
也是一把互斥锁,但是它是互斥锁中的递归锁,所谓递归在锁的第一篇文章中就已经提到了,在同一线程中可以再次获取锁去加锁,而不会造成死锁。简单来说就是加锁执行一段代码,但是代码中调用的另一段代码也要获取这个锁去加锁执行,这里就递归锁应用的地方。
1.1 NSRecursiveLock 定义
@interface NSRecursiveLock : NSObject {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
这里跟NSLock
的定义差不多
- 同样遵守
NSLocking
协议 -
*_priv
私有成员变量和name
属性。 -
tryLock
尝试获取锁,获取到返回YES
,否则返回NO
。不会阻塞线程。 -
lockBeforeDate:
尝试在指定时间前获取锁,获取到返回YES
,否则返回NO
,在指定时间前会阻塞线程。
1.2 NSRecursiveLock 源码探索
我们同样来到Swift CoreLibs Foundation源码一探究竟。
open class NSRecursiveLock: NSObject, NSLocking {
internal var mutex = _RecursiveMutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif
public override init() {
super.init()
#if os(Windows)
InitializeCriticalSection(mutex)
InitializeConditionVariable(timeoutCond)
InitializeSRWLock(timeoutMutex)
#else
#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
}
deinit {
#if os(Windows)
DeleteCriticalSection(mutex)
#else
pthread_mutex_destroy(mutex)
#endif
mutex.deinitialize(count: 1)
mutex.deallocate()
#if os(macOS) || os(iOS) || os(Windows)
deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
#endif
}
open func lock() {
#if os(Windows)
EnterCriticalSection(mutex)
#else
pthread_mutex_lock(mutex)
#endif
}
open func unlock() {
#if os(Windows)
LeaveCriticalSection(mutex)
AcquireSRWLockExclusive(timeoutMutex)
WakeAllConditionVariable(timeoutCond)
ReleaseSRWLockExclusive(timeoutMutex)
#else
pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
#endif
#endif
}
open func `try`() -> Bool {
#if os(Windows)
return TryEnterCriticalSection(mutex)
#else
return pthread_mutex_trylock(mutex) == 0
#endif
}
open func lock(before limit: Date) -> Bool {
#if os(Windows)
if TryEnterCriticalSection(mutex) {
return true
}
#else
if pthread_mutex_trylock(mutex) == 0 {
return true
}
#endif
#if os(macOS) || os(iOS) || os(Windows)
return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
#else
guard var endTime = timeSpecFrom(date: limit) else {
return false
}
return pthread_mutex_timedlock(mutex, &endTime) == 0
#endif
}
open var name: String?
}
其实乍一看NSRecursiveLock
源码跟NSLock
差不多,但是仔细一看就能发现在NSRecursiveLock
中对于互斥锁mutex
设置了递归属性,主要体现在如下两个地方:
其他的调用和方法实现跟NSLock
几乎一致,可以参考我的这篇文章,这里只是对底层pthread_mutex
使用了递归属性的锁,具体pthread_mutex
内部是什么样的实现的以及互斥锁是否可以递归,可以试着去研究一下pthread
或者POSIX
。
1.3 NSRecursiveLock 使用示例
1.3.1 示例1
其实NSRecursiveLock
最合适的应用场景就是解决同一把锁在同一线程内多次加锁的问题,这正好解决我们在分析NSLock
这篇文章时的死锁问题。
- (void)viewDidLoad {
[super viewDidLoad];
self.recursiveLock = [[NSRecursiveLock alloc] init];
[NSThread detachNewThreadSelector:@selector(testRecursiveLock1) toTarget:self withObject:nil];
}
- (void)testRecursiveLock1 {
[self.recursiveLock lock];
NSLog(@"testRecursiveLock1");
[self testRecursiveLock2];
[self.recursiveLock unlock];
NSLog(@"testRecursiveLock1: unlock");
}
- (void)testRecursiveLock2 {
[self.recursiveLock lock];
NSLog(@"testRecursiveLock2");
[self.recursiveLock unlock];
NSLog(@"testRecursiveLock2: unlock");
}
如上面的代码,如果我们使用NSLock
就会造成死锁。这里我们使用NSRecursiveLock
,打印结果如下:
可以看到我们加锁执行1,然后在执行2,2解锁后返回到1在进行解锁。
1.3.2 示例2
这里通过block的递归调用来演示NSRecursiveLock
的作用。
- (void)testRecursive{
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
static void (^testMethod)(int);
for (int i= 0; i<100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d--- 线程%@",value, [NSThread currentThread]);
testMethod(value - 1);
}
[lock unlock];
};
testMethod(10);
});
}
}
打印结果:
1.4 死锁
那么递归锁会有死锁吗?答案是肯定的,在实例2中的代码加个循环就会造成死锁:
- (void)testRecursive2{
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
static void (^testMethod)(int);
for (int i = 0; i < 2; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d--- 线程%@",value, [NSThread currentThread]);
testMethod(value - 1);
}
[lock unlock];
};
testMethod(10);
});
}
}
此时将锁换成@synchronized
:
- (void)testRecursive2{
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
static void (^testMethod)(int);
for (int i = 0; i < 2; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
testMethod = ^(int value){
// [lock lock];
// if (value > 0) {
// NSLog(@"current value = %d--- 线程%@",value, [NSThread currentThread]);
// testMethod(value - 1);
// }
//
// [lock unlock];
@synchronized (self) {
if (value > 0) {
NSLog(@"current value = %d--- 线程%@",value, [NSThread currentThread]);
testMethod(value - 1);
}
}
};
testMethod(10);
});
}
}
此时就不会造成死锁了,因为@synchronized
是对递归锁进行了一次封装,通过哈希表对存储锁对象,对已经加锁的对象不再加锁,而只是增加lockCount
。
2. atomic
我们在开发中经常会用到属性,也会经常写nonatomic
,这是非原子属性,那么原子属性和非原子属性之间有什么区别呢?atomic
到底是怎样实现属性的原子的性的呢?下面我们来探究一下。
2.1 寻找底层实现
由于atomic
是关键字,我们不能通过点击跳转的方式去查看它的实现。因为声明属性这个语法是OC
特有的,属性的本质也就是setter
和getter
,所以我们可以直接来到objc
的源码中一探究竟。这里使用的是objc4-779.1
源码。
2.1.1 setter
我们先看set
方法中的实现:我们全局搜索setProperty
我们发现,所有setProperty
方法最终都会调用reallySetProperty
方法。其实我们可以写个属性赋,然后断点跟一下是否会调用reallySetProperty
方法,这里就不验证了,肯定会调用的。我们直接看reallySetProperty
的实现部分,代码如下:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
该方法一共有7个参数:
-
self
:这里存储的就是对象 -
_cmd
:方法名称,这里是setXxx:
-
newValue
:要赋的新值 -
offset
:指针偏移量。对象的指针就是isa
的地址,当我们有多个属性的时候,会根据偏移量一个一个去内存地址中找这个属性 -
atomic
:是否是原子性,跟我们编码时写的atomic
和nonatomic
保持一致 -
copy
:是否是拷贝 -
mutableCopy
:是否是可变类型的拷贝
分析完参数我们来分析源码:
- 首先是判断偏移量是否为0,如果是0就是修改
isa
,此处调用object_setClass
后就直接返回 - 根据偏移量获取属性地址
slot
- 根据是否是
copy
和mutableCopy
来拷贝newValue
或者objc_retain(newValue)
- 根据是否是原子性的进行处理,都会记录旧值后面进行释放,然后赋新值
- 如果是原子性的就通过
spinlock_t
进行加锁进行赋值
2.1.2 getter
看完setter
后我们在来看看getter
,这里取值是调用的objc_getProperty
方法,源码如下:
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
没啥多说的,同样是判断是否是atomic
,不是就直接根据指针和偏移量取到的值直接返回,如果是atomic
,就加锁spinlock_t
获取值,并进行objc_retain
,返回时是返回的自动释放对象objc_autoreleaseReturnValue(value)
,这里还有一句注释(为了提高性能,我们(安全地)在自旋锁之外发布自动释放。)
2.1.3 spinlock_t
关于spinlock_t
在我的关于锁的第一篇文章中iOS 中的锁(1)中关于@synchronized
的分析中有详细的讲解,可以移步那里看一看。
2.2 关于线程安全
那么原子属性真的安全吗?我们先来测试一下:
2.2.1 示例一
@interface ViewController ()
@property (atomic) NSInteger number;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100; i++) {
self.number = self.number + 1;
NSLog(@"number: %ld", self.number);
}
});
// 线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100; i++) {
self.number = self.number + 1;
NSLog(@"number: %ld", self.number);
}
});
}
@end
按照我们的预期,这里应该打印到200,但是只到199,我向上翻阅看到如下结果:
从上图可以看到9 被打印了两次。
2.2.1 示例二
下面我们来测试一下关于数组使用atomic
时的线程安全。
@interface ViewController ()
@property (atomic, strong) NSArray *array;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.array = @[@"aa", @"bb", @"cc"];
} else {
self.array = @[@"dd"];
}
NSLog(@"线程1: %@\n", self.array);
}
});
//线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100000; i ++) {
if (self.array.count >= 2) {
NSString* str = [self.array objectAtIndex:1];
NSLog(@"取到的值是:%@",str);
}
NSLog(@"线程2: %@\n",self.array);
}
});
}
@end
这里我们看到直接由于数组越界导致程序崩溃了。
2.2.3 小结
由上面两个例子的打印结果可以看出atomic
并不是线程安全的,它只是保证setter
和getter
方法内是安全的,一旦出了方法就需要外部控制了
比如我们的+1
操作并没有加锁,所以就造成了值没有达到我们预期的结果,这里实际上就是两个线程同时取到了相同的值,在计算完后赋值的时候也都是一样的,这种情况多了值也就不是预期的了。
又比如在示例二中我们对数组的不断切换中,对数组取长度的时候是对的,但是在这之后数组长度变了,再去取值就会造成数组越界,导致程序崩溃。
所以说使用atomic
修饰的属性时并不是绝对线程安全的,它只保证在setter
和getter
方法内是安全的,一旦出了方法,属性的安全性就得由程序员负责了。