iOS 中的锁(3)

iOS 中的锁(3)

不想篇幅太长,再开一篇继续探究iOS中的锁。

注:本文主要通过Objective-C语言进行体现,其实跟Swift也差不多。

本文介绍了iOS中的NSRecursiveLockatomic两种锁。

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设置了递归属性,主要体现在如下两个地方:

iOS 中的锁(3)_第1张图片
16049754888999.jpg

其他的调用和方法实现跟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,打印结果如下:

iOS 中的锁(3)_第2张图片
打印结果.jpg

可以看到我们加锁执行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);
        });
    }
}

打印结果:

iOS 中的锁(3)_第3张图片
16049790724029.jpg

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);
        });
    }
}
iOS 中的锁(3)_第4张图片
16049915755563.jpg

此时将锁换成@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特有的,属性的本质也就是settergetter,所以我们可以直接来到objc的源码中一探究竟。这里使用的是objc4-779.1源码。

2.1.1 setter

我们先看set方法中的实现:我们全局搜索setProperty

iOS 中的锁(3)_第5张图片
setProperty.jpg

我们发现,所有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个参数:

  1. self:这里存储的就是对象
  2. _cmd:方法名称,这里是setXxx:
  3. newValue:要赋的新值
  4. offset:指针偏移量。对象的指针就是isa的地址,当我们有多个属性的时候,会根据偏移量一个一个去内存地址中找这个属性
  5. atomic:是否是原子性,跟我们编码时写的atomicnonatomic保持一致
  6. copy:是否是拷贝
  7. mutableCopy:是否是可变类型的拷贝

分析完参数我们来分析源码:

  • 首先是判断偏移量是否为0,如果是0就是修改isa,此处调用object_setClass后就直接返回
  • 根据偏移量获取属性地址slot
  • 根据是否是copymutableCopy来拷贝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
iOS 中的锁(3)_第6张图片
打印结果1.jpg

按照我们的预期,这里应该打印到200,但是只到199,我向上翻阅看到如下结果:

iOS 中的锁(3)_第7张图片
打印结果2.jpg

从上图可以看到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
iOS 中的锁(3)_第8张图片
16050632389125.jpg

这里我们看到直接由于数组越界导致程序崩溃了。

2.2.3 小结

由上面两个例子的打印结果可以看出atomic并不是线程安全的,它只是保证settergetter方法内是安全的,一旦出了方法就需要外部控制了

比如我们的+1操作并没有加锁,所以就造成了值没有达到我们预期的结果,这里实际上就是两个线程同时取到了相同的值,在计算完后赋值的时候也都是一样的,这种情况多了值也就不是预期的了。

又比如在示例二中我们对数组的不断切换中,对数组取长度的时候是对的,但是在这之后数组长度变了,再去取值就会造成数组越界,导致程序崩溃。

所以说使用atomic修饰的属性时并不是绝对线程安全的,它只保证在settergetter方法内是安全的,一旦出了方法,属性的安全性就得由程序员负责了。

你可能感兴趣的:(iOS 中的锁(3))