iOS進階-細數iOS中的鎖

鎖的種類

互斥鎖 自旋鎖

  • 互斥鎖:保證在任什麼時候候,都只有一個線程訪問對象。當獲取鎖操做失敗時,線程會進入睡眠,等待鎖釋放時被喚醒;ios

  • 自旋鎖:與互斥鎖有點相似,只是自旋鎖 不會引發調用者睡眠,若是自旋鎖已經被別的執行單元保持,調用者就一直循環嘗試,直到該自旋鎖的保持者已經釋放了鎖;由於不會引發調用者睡眠,因此效率高於互斥鎖;程序員

  • 自旋鎖缺點:面試

  1. 調用者在未得到鎖的狀況下,一直運行--自旋,因此佔用着CPU,若是不能在很短的時間內得到鎖,會使CPU效率下降。因此自旋鎖就主要用在臨界區持鎖時間很是短且CPU資源不緊張的狀況下
  2. 在用自旋鎖時有可能形成死鎖,當遞歸調用時有可能形成死鎖

兩種鎖的加鎖原理

  • 互斥鎖:線程會從sleep(加鎖)——>running(解鎖),過程當中有上下文的切換,cpu的搶佔,信號的發送等開銷。
  • 自旋鎖:線程一直是running(加鎖——>解鎖),死循環檢測鎖的標誌位,機制不復雜。

遞歸鎖

特殊的互斥鎖,加了遞歸功能swift

iOS中的鎖

1.@synchronized

@synchronized (self) {
要鎖的代碼
}
複製代碼

打斷點使用匯編查看內部實現,可看到被執行的代碼會被下文中的兩句代碼包裹安全

objc_sync_enter
要鎖的代碼
objc_sync_exit
複製代碼

在objc中源碼查看bash

######### 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;
}

######### 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;
複製代碼

@synchronized結論:多線程

  • 是對互斥鎖的一種封裝
  • 具體點是種特殊的互斥鎖->遞歸鎖,內部搭配 nil防止死鎖
  • 經過的結構存要鎖的對象
  • 表內部的對象又是經過哈希存儲的

坑點:在大量線程異步同時操做同一個對象時,由於遞歸鎖會不停的alloc/release,在某一個對象會是nil;而此時 @synchronized (obj) 會判斷obj==nil,就不會再加鎖,致使線程訪問衝突;eg併發

#import "KTest.h"
@interface KTest()
@property (nonatomic,strong) NSMutableArray *testArray;

@end

@implementation KTest

- (void)crash {
    //_testArray
    //nil 不加鎖 - old release
    //hash objc - nil
    for (int i = 0; i < 20000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (_testArray) {//在某一刻_testArray= nil,致使加鎖失敗
                _testArray = [NSMutableArray array];
            }
        });
    }
}
//解決NSLock
- (void)NO_crash {
    NSLock *lock = [[NSLock alloc] init];
    for (int i = 0; i < 20000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            _testArray = [NSMutableArray array];
            [lock unlock];
        });
    }
}
@end
複製代碼

2.NSLock

在上面的例子裏咱們用NSLock去解決在大量線程異步同時操做同一個對象的內存安全問題;那咱們細看下NSLock的源碼,NSLock屬於Foundation,須要在Foundation中查找,我這裏是Swift版本的Foundation,我對源碼作了一些簡化方便查看異步

open class NSLock: NSObject, NSLocking {
    internal var mutex = _MutexPointer.allocate(capacity: 1)
    private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
    private var timeoutMutex = _MutexPointer.allocate(capacity: 1)

    public override init() {
        pthread_mutex_init(mutex, nil)
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
    }
    
    deinit {
        pthread_mutex_destroy(mutex)
        mutex.deinitialize(count: 1)
        mutex.deallocate()
        deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
    }
    
    open func lock() {
        pthread_mutex_lock(mutex)
    }

    open func unlock() {
        pthread_mutex_unlock(mutex)
        // Wakeup any threads waiting in lock(before:)
        pthread_mutex_lock(timeoutMutex)
        pthread_cond_broadcast(timeoutCond)
        pthread_mutex_unlock(timeoutMutex)
    }

    open func `try`() -> Bool {
        return pthread_mutex_trylock(mutex) == 0
    }
    
    open func lock(before limit: Date) -> Bool {
        if pthread_mutex_trylock(mutex) == 0 {
            return true
        }
        return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
    }
    open var name: String?
}
複製代碼

上面源碼可看出:async

  • NSLock是對pthread_mutex的封裝
  • NSLock還有timeout超時控制 坑點:當NSLock對同一個線程鎖兩次,就會形成死鎖;即不能實現遞歸鎖,這種狀況須要用到NSRecursiveLock 先看官方文檔上的話
    NSLock
    遞歸調用示例:
//NSLock
- (void)NSLock_crash {
    NSLock *lock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testBlock)(int);
        testBlock = ^(int value) {
            [lock lock];
            if (value > 0) {
                NSLog(@"value-->%d",value);
                testBlock(value-1);//遞歸調用,用遞歸鎖
            }
            [lock unlock];
        };
        testBlock(10);
    });
}

//遞歸鎖NSRecursiveLock
- (void)NSRecursiveLock_NO_crash {
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testBlock)(int);
        testBlock = ^(int value) {
            [lock lock];
            if (value > 0) {
                NSLog(@"value-->%d",value);
                testBlock(value-1);//遞歸調用,用遞歸鎖
            }
            [lock unlock];
        };
        testBlock(10);
    });
}
複製代碼

3.NSRecursiveLock

在上面的例子已經說明了NSRecursiveLock可以處理遞歸調用;可是仍是要看看源碼

open class NSRecursiveLock: NSObject, NSLocking {
    internal var mutex = _RecursiveMutexPointer.allocate(capacity: 1)
    private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
    private var timeoutMutex = _MutexPointer.allocate(capacity: 1)

    public override init() {
        super.init()
        var attrib = pthread_mutexattr_t()
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
    }
    
    deinit {
        pthread_mutex_destroy(mutex)
        mutex.deinitialize(count: 1)
        mutex.deallocate()
        deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
    }
    
    open func lock() {
        pthread_mutex_lock(mutex)
    }
    
    open func unlock() {
        pthread_mutex_unlock(mutex)
        // Wakeup any threads waiting in lock(before:)
        pthread_mutex_lock(timeoutMutex)
        pthread_cond_broadcast(timeoutCond)
        pthread_mutex_unlock(timeoutMutex)
    }
    
    open func `try`() -> Bool {
        return pthread_mutex_trylock(mutex) == 0
    }
    
    open func lock(before limit: Date) -> Bool {
        if pthread_mutex_trylock(mutex) == 0 {
            return true
        }
        return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
    }
    open var name: String?
}
複製代碼

上面源碼可看出:

  • NSRecursiveLock也是對pthread_mutex的封裝,不一樣的是加Recursive遞歸調用功能;
  • NSRecursiveLock一樣也有timeout超時控制

4. NSCondition

相對來講NSCondition用的比較少,但也須要了解。先看源碼

open class NSCondition: NSObject, NSLocking {
    internal var mutex = _MutexPointer.allocate(capacity: 1)
    internal var cond = _ConditionVariablePointer.allocate(capacity: 1)

    public override init() {
        pthread_mutex_init(mutex, nil)
        pthread_cond_init(cond, nil)
    }
    
    deinit {
        pthread_mutex_destroy(mutex)
        pthread_cond_destroy(cond)
        mutex.deinitialize(count: 1)
        cond.deinitialize(count: 1)
        mutex.deallocate()
        cond.deallocate()
    }
    
    open func lock() {
        pthread_mutex_lock(mutex)
    }
    
    open func unlock() {
        pthread_mutex_unlock(mutex)
    }
    
    open func wait() {
        pthread_cond_wait(cond, mutex)
    }

    open func wait(until limit: Date) -> Bool {
        guard var timeout = timeSpecFrom(date: limit) else {
            return false
        }
        return pthread_cond_timedwait(cond, mutex, &timeout) == 0
    }
    
    open func signal() {
        pthread_cond_signal(cond)
    }
    
    open func broadcast() {
        // 彙編分析 - 猜 (多看多玩)
        pthread_cond_broadcast(cond) // wait  signal
    }
    open var name: String?
}
複製代碼

由上面源碼能夠看出:

  • NSCondition 也是對pthread_mutex的封裝
  • 使用wait信號可讓當前線程處於等待中
  • 使用signal信號能夠告訴其餘某一個線程不用再等待了,能夠繼續執行
  • 內部還有一個broadcast(廣播)信號,用於發送(signal)信號給其餘全部線程 用法:
NSCondition *lock = [[NSCondition alloc] init];
    NSMutableArray *array = [[NSMutableArray alloc] init];
    //線程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        while (!array.count) {
            [lock wait];
        }
        [array removeAllObjects];
        NSLog(@"array removeAllObjects");
        [lock unlock];
    });

    //線程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);//以保證讓線程2的代碼後執行
        [lock lock];
        [array addObject:@1];
        NSLog(@"array addObject:@1");
        [lock signal];
        [lock unlock];
    });
複製代碼

5.NSConditionLock相似於信號量

先看源碼

open class NSConditionLock : NSObject, NSLocking {
    internal var _cond = NSCondition()
    internal var _value: Int
    internal var _thread: _swift_CFThreadRef?
    
    public convenience override init() {
        self.init(condition: 0)
    }
    
    public init(condition: Int) {
        _value = condition
    }

    open func lock() {
        let _ = lock(before: Date.distantFuture)
    }

    open func unlock() {
        _cond.lock()
        _thread = nil
        _cond.broadcast()
        _cond.unlock()
    }
    
    open var condition: Int {
        return _value
    }

    open func lock(whenCondition condition: Int) {
        let _ = lock(whenCondition: condition, before: Date.distantFuture)
    }

    open func `try`() -> Bool {
        return lock(before: Date.distantPast)
    }
    
    open func tryLock(whenCondition condition: Int) -> Bool {
        return lock(whenCondition: condition, before: Date.distantPast)
    }

    open func unlock(withCondition condition: Int) {
        _cond.lock()
        _thread = nil
        _value = condition
        _cond.broadcast()
        _cond.unlock()
    }

    open func lock(before limit: Date) -> Bool {
        _cond.lock()
        while _thread != nil {
            if !_cond.wait(until: limit) {
                _cond.unlock()
                return false
            }
        }
        _thread = pthread_self()
        _cond.unlock()
        return true
    }
    
    open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
        _cond.lock()
        while _thread != nil || _value != condition {
            if !_cond.wait(until: limit) {
                _cond.unlock()
                return false
            }
        }
        _thread = pthread_self()
        _cond.unlock()
        return true
    }
    
    open var name: String?
}
複製代碼

由源碼可知:

  • NSConditionLock 是 對NSCondition+線程數的封裝,即NSConditionLock = NSCondition + lock
  • internal var _thread: _swift_CFThreadRef?:_thread就是當前能夠同事操做的線程數,經過搭配NSCondition能夠達到dispatch_semaphore的效果
  • lock(before: Date.distantFuture):也有超時時間 用法示例:
#pragma mark -- 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];
       
       NSLog(@"線程 2");
       
       [conditionLock unlockWithCondition:1];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
       [conditionLock lock];
       NSLog(@"線程 3");
       [conditionLock unlock];
    });
}
複製代碼

6.dispatch_semaphore

dispatch_semaphore 是 GCD 用來同步的一種方式,與他相關的只有三個函數,一個是建立信號量,一個是等待信號,一個是發送信號。

dispatch_semaphore_create(long value);
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
複製代碼

dispatch_semaphore 和 NSConditionLock 相似,都是一種基於信號的同步方式,但 NSCondition 信號只能發送,不能保存(若是沒有線程在等待,則發送的信號會失效)。而 dispatch_semaphore 能保存發送的信號。dispatch_semaphore 的核心是 dispatch_semaphore_t 類型的信號量。 dispatch_semaphore_create(1) 方法能夠建立一個 dispatch_semaphore_t 類型的信號量,設定信號量的初始值爲 1。注意,這裏的傳入的參數必須大於或等於 0,不然 dispatch_semaphore_create 會返回 NULL。 dispatch_semaphore_wait(signal, overTime); 方法會判斷 signal 的信號值是否大於 0。大於 0 不會阻塞線程,消耗掉一個信號,執行後續任務。若是信號值爲 0,該線程會和 NSCondition 同樣直接進入 waiting 狀態,等待其餘線程發送信號喚醒線程去執行後續任務,或者當 overTime 時限到了,也會執行後續任務。 dispatch_semaphore_signal(signal); 發送信號,若是沒有等待的線程接受信號,則使 signal 信號值加一(作到對信號的保存)。

用法:

- (void)testDispatch_semaphore_t {
    dispatch_semaphore_t signal = dispatch_semaphore_create(1);
    dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_semaphore_wait(signal, overTime);
        sleep(2);
        NSLog(@"線程1");
        dispatch_semaphore_signal(signal);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        dispatch_semaphore_wait(signal, overTime);
        NSLog(@"線程2");
        dispatch_semaphore_signal(signal);
    });
}
複製代碼

從上面的實例代碼能夠看到,一個 dispatch_semaphore_wait(signal, overTime); 方法會去對應一個 dispatch_semaphore_signal(signal); 看起來像 NSLock 的 lock 和 unlock,其實能夠這樣理解,區別只在於有信號量這個參數,lock unlock 只能同一時間,一個線程訪問被保護的臨界區,而若是 dispatch_semaphore 的信號量初始值爲 x ,則能夠有 x 個線程同時訪問被保護的臨界區。

7.OSSpinLock - os_unfair_lock

在iOS10 以前,OSSpinLock 是一種自旋鎖,也只有加鎖,解鎖,嘗試加鎖三個方法。和 NSLock 不一樣的是 NSLock 請求加鎖失敗的話,會先輪詢,但一秒事後便會使線程進入 waiting 狀態,等待喚醒。而 OSSpinLock 會一直輪詢,等待時會消耗大量 CPU 資源,不適用於較長時間的任務。而由於OSSpinLock再也不線程安全,在iOS10以後OSSpinLock被廢棄內部封裝了os_unfair_lockos_unfair_lock也是一種互斥鎖不會忙等。 typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);

bool    OSSpinLockTry( volatile OSSpinLock *__lock );
void    OSSpinLockLock( volatile OSSpinLock *__lock );
void    OSSpinLockUnlock( volatile OSSpinLock *__lock );
複製代碼

用例示範:

__block OSSpinLock theLock = OS_SPINLOCK_INIT;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(&theLock);
        NSLog(@"線程1");
        sleep(10);
        OSSpinLockUnlock(&theLock);
        NSLog(@"線程1解鎖成功");
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        OSSpinLockLock(&theLock);
        NSLog(@"線程2");
        OSSpinLockUnlock(&theLock);
    });

2016-08-19 20:25:13.526 ThreadLockControlDemo[2856:316247] 線程1
2016-08-19 20:25:23.528 ThreadLockControlDemo[2856:316247] 線程1解鎖成功
2016-08-19 20:25:23.529 ThreadLockControlDemo[2856:316260] 線程2
複製代碼

8.讀寫鎖

讀寫鎖是一種特殊的的自旋鎖;它能作到多讀單寫; 實現: 併發隊列 + dispatch_barrier_async

########### .h文件
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface RF_RWLock : NSObject
// 讀數據
- (id)rf_objectForKey:(NSString *)key;
// 寫數據
- (void)rf_setObject:(id)obj forKey:(NSString *)key;
@end

NS_ASSUME_NONNULL_END

########### .m文件
#import "RF_RWLock.h"

@interface RF_RWLock ()
// 定義一個併發隊列:
@property (nonatomic, strong) dispatch_queue_t concurrent_queue;
// 用戶數據中心, 可能多個線程須要數據訪問:
@property (nonatomic, strong) NSMutableDictionary *dataCenterDic;
@end

@implementation RF_RWLock

- (id)init{
    self = [super init];
    if (self){
        // 建立一個併發隊列:
        self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 建立數據字典:
        self.dataCenterDic = [NSMutableDictionary dictionary];
    }
    return self;
}

#pragma mark - 讀數據
- (id)rf_objectForKey:(NSString *)key{
    __block id obj;
    // 同步讀取指定數據:
    dispatch_sync(self.concurrent_queue, ^{
        obj = [self.dataCenterDic objectForKey:key];
    });
    return obj;
}

#pragma mark - 寫數據
- (void)rf_setObject:(id)obj forKey:(NSString *)key{
    // 異步柵欄調用設置數據:
    dispatch_barrier_async(self.concurrent_queue, ^{
        [self.dataCenterDic setObject:obj forKey:key];
    });
}

@end
複製代碼

面試題

atomic

1.atomic的原理?

atomic 在對象get/set的時候,會有一個spinlock_t控制。即當兩個線程A和B,若是A正在執行getter時,B若是想要執行settet,就要等A執行getter完成後才能執行

2.atomic修飾的屬性絕對安全嗎?

  • atomic 只保證set/get方法安全,可是當多個線程不使用set/get方法訪問時,就再也不安全;
  • 因此atomic屬性和property的多線程安全並無什麼直接的聯繫,多線程安全仍是要程序員本身保障
  • atomic的因爲使用了自旋鎖,性能比nonatomic慢20倍

參考 iOS 中的八大鎖

相關文章
相關標籤/搜索