iOS 鎖

weixin_34019929發表於2018-01-06

1400498-2bfca138992bb7d8.png
各種鎖的效能較

鎖是用來保證執行緒安全的一種機制,也是保持資料同步的一種必要手段。是確保一段程式碼在同一個時間只能允許被一個執行緒訪問,例如,執行緒A進入一段被鎖Lock加鎖的程式碼,另外一個執行緒B,就不能在此時訪問,只能等待執行緒A執行完成後,B執行緒才可以訪問該段加鎖的程式碼。


Advise:不要將過多的操作放到加鎖的程式碼裡,而讓另外一個執行緒長時間的等待,這樣不利於發揮多執行緒應有的功能。

鎖的型別

1. NSLock

NSLock 是一個互斥鎖,實現了NSLocking<待查詢該協議的內容>協議。

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end

  • lock進行加鎖;
  • unlock解鎖;
  • tryLock嘗試加鎖,如果失敗,並不會阻塞執行緒,只是立即返回,不會執行加鎖程式碼;
  • lockBeforeDate:在指定時間date之前阻塞執行緒,如果到期沒有獲取到鎖,則執行緒被喚醒,函式立即返回NO。
2. 條件鎖NSCondition

NSCondition條件鎖,它也實現了NSLocking協議,所以也有lockunlock方法。當然NSCondition還有更高階的用法,有wait和signal。
NSCondition可以給每個分執行緒加鎖,加鎖後不影響其他執行緒進入臨界區域。這種分別加鎖的方式,wait並加鎖後不能真正解決資源競爭。

@interface NSCondition : NSObject <NSLocking> {
@private
    void *_priv;
}

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end

-(void)wait;進入等待狀態
-(BOOL)waitUnitilDate:(NSDate *)date;執行緒等待一定時間
-(void)signal;隨機喚醒一個執行緒取消等待繼續執行
-(void)broadcast; 喚醒所有執行緒取消等待繼續執行

NSConditionLock也實現了NSLocking協議,可以像NSCondition一樣做多執行緒之間的任務等待呼叫,且執行緒安全

@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end
3. 遞迴鎖NSRecursiveLock

在遞迴呼叫,遞迴開始前加鎖,遞迴呼叫開始後會重複執行此方法以至於反覆執行加鎖程式碼最終造成死鎖,這個時候可以使用遞迴鎖來解決。使用遞迴鎖可以在一個執行緒中反覆獲取鎖而不造成死鎖,這個過程中會記錄獲取鎖和釋放鎖的次數,只有最後兩者平衡鎖才被最終釋放。

@interface NSRecursiveLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end

4. NSDistributedLock 沒仔細研究過

NSDistributedLock是MAC開發中的跨程式的分散式鎖,底層是用檔案系統實現的互斥鎖。NSDistributedLock沒有實現NSLocking協議,所以沒有lock方法,取而代之的是非阻塞的tryLock方法。當執行到do something時程式退出,程式再次啟動之後tryLock就再也不能成功了,陷入死鎖狀態.其他應用也不能訪問受保護的共享資源。在這種情況下,你可以使用breadLock方法來打破現存的鎖以便你可以獲取它。但是通常應該避免打破鎖,除非你確定擁有程式已經死亡並不可能再釋放該鎖。

. 5@synchronized程式碼塊
- (void)TeseSynchronizedMethod{
    @synchronized(self){
        //加鎖的程式碼塊
    }
}

以上加鎖的方式,把對應的程式碼塊加到對應的塊{}裡邊,如果某個執行緒沒有執行完,其他的執行緒需要執行就得等待。類似互斥鎖,保證此時沒有其他執行緒對self物件進行修改。
@synchronized是objective-c的一個鎖令牌,防止self物件在同一時間被其他執行緒訪問,起到執行緒的保護作用。一般在公有變數的時候作用,例如單例模式或者操作類的static變數中使用。
指令@synchronized()需要一個引數。該引數可以使任何的Objective-C物件,包括self。這個物件就是互斥訊號量。針對程式中的不同的關鍵程式碼段,分別使用不同的訊號量。只有在應用程式程式設計執行多執行緒之前就建立好所有需要的互斥訊號量物件來避免執行緒間的競爭才是最安全的。
Objective-C中的同步特性是支援遞迴的。一個執行緒是可以以遞迴的方式多次使用同一個訊號量的;其他的執行緒會被阻塞知道這個執行緒釋放了自己所有的和該訊號量相關的鎖,也就是說通過正常執行或者是通過異常處理的方式退出了所有的@synchronized()程式碼塊。
當在@synchronized()程式碼塊中丟擲異常的時候, Objective-C執行時會捕獲到該異常,並釋放訊號量,並把該異常重新丟擲給下一個異常處理者。

. 6dispatch_semaphore 訊號量

dispatch_semaphore是GCD中的訊號量,它可以解決資源競爭和訊號的通知和等待。當傳送一個訊號通知,則訊號量+1;當等待一個訊號時訊號量-1;如果訊號量為0,則訊號會處於等待的狀態,直到訊號量大於0開始執行。
來個例子:

dispatch_semaphore_t signal = dispatch_semaphore_create(1); //傳入值必須 >=0, 若傳入為0則阻塞執行緒並等待timeout,時間到後會執行其後的語句
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);

//執行緒1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"執行緒1 等待ing");
    dispatch_semaphore_wait(signal, overTime); //signal 值 -1
    NSLog(@"執行緒1");
    dispatch_semaphore_signal(signal); //signal 值 +1
    NSLog(@"執行緒1 傳送訊號");
    NSLog(@"--------------------------------------------------------");
});

//執行緒2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"執行緒2 等待ing");
    dispatch_semaphore_wait(signal, overTime);
    NSLog(@"執行緒2");
    dispatch_semaphore_signal(signal);
    NSLog(@"執行緒2 傳送訊號");
});
7. 互斥鎖POSIX

POSIX是Unix/Linux平臺上提供的一套條件互斥鎖的API。
新建一個簡單的POSIX互斥鎖,引入標頭檔案#import <pthread.h>宣告並初始化一個pthread_mutex_t的結構。使用pthread_mutex_lockpthread_mutex_unlock函式。呼叫pthread_mutex_destroy來釋放該鎖的資料結構。

int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);

int pthread_mutex_lock(pthread_mutex_t *);

int pthread_mutex_trylock(pthread_mutex_t *);

int pthread_mutex_unlock(pthread_mutex_t *);

int pthread_mutex_destroy(pthread_mutex_t *);

int pthread_mutex_setprioceiling(pthread_mutex_t * __restrict, int,
  int * __restrict);

int pthread_mutex_getprioceiling(const pthread_mutex_t * __restrict,
  int * __restrict);

int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);
首先是第一個方法,這是初始化一個鎖,__restrict 為互斥鎖的型別,傳 NULL 為預設型別,一共有 4 型別。

PTHREAD_MUTEX_NORMAL 預設型別,也就是普通鎖。當一個執行緒加鎖以後,其餘請求鎖的執行緒將形成一個等待佇列,並在解鎖後先進先出原則獲得鎖。

PTHREAD_MUTEX_ERRORCHECK 檢錯鎖,如果同一個執行緒請求同一個鎖,則返回 EDEADLK,否則與普通鎖型別動作相同。這樣就保證當不允許多次加鎖時不會出現巢狀情況下的死鎖。

PTHREAD_MUTEX_RECURSIVE 遞迴鎖,允許同一個執行緒對同一個鎖成功獲得多次,並通過多次 unlock 解鎖。

PTHREAD_MUTEX_DEFAULT 適應鎖,動作最簡單的鎖型別,僅等待解鎖後重新競爭,沒有等待佇列。

OSIX還可以建立條件鎖,提供了和NSCondition一樣的條件控制,初始化互斥鎖同時使用pthread_cond_initt來初始化條件資料結構.

    // 初始化
    int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
    
    // 等待(會阻塞)
    int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
    
    // 定時等待
    int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
    
    // 喚醒
    int pthread_cond_signal (pthread_cond_t *cond);
    
    // 廣播喚醒
    int pthread_cond_broadcast (pthread_cond_t *cond);
    
    // 銷燬
    int pthread_cond_destroy (pthread_cond_t *cond);

POSIX還提供了很多函式,有一套完整的API,包含Pthreads執行緒的建立控制等等,非常底層,可以手動處理執行緒的各個狀態的轉換即管理生命週期,甚至可以實現一套自己的多執行緒,感興趣的可以繼續深入瞭解。推薦一篇詳細文章,但不是基於iOS的,是基於Linux的,但是介紹的非常詳細 Linux 執行緒鎖詳解

8. 自旋鎖OSSpinLock
typedef int32_t OSSpinLock;

bool    OSSpinLockTry( volatile OSSpinLock *__lock );

void    OSSpinLockLock( volatile OSSpinLock *__lock );

void    OSSpinLockUnlock( volatile OSSpinLock *__lock );

首先要提的是OSSpinLock已經出現了BUG,導致並不能完全保證是執行緒安全的。

新版 iOS 中,系統維護了 5 個不同的執行緒優先順序/QoS: background,utility,default,user-initiated,user-interactive。高優先順序執行緒始終會在低優先順序執行緒前執行,一個執行緒不會受到比它更低優先順序執行緒的干擾。這種執行緒排程演算法會產生潛在的優先順序反轉問題,從而破壞了 spin lock。
具體來說,如果一個低優先順序的執行緒獲得鎖並訪問共享資源,這時一個高優先順序的執行緒也嘗試獲得這個鎖,它會處於 spin lock 的忙等狀態從而佔用大量 CPU。此時低優先順序執行緒無法與高優先順序執行緒爭奪 CPU 時間,從而導致任務遲遲完不成、無法釋放 lock。這並不只是理論上的問題,libobjc 已經遇到了很多次這個問題了,於是蘋果的工程師停用了 OSSpinLock。
蘋果工程師 Greg Parker 提到,對於這個問題,一種解決方案是用 truly unbounded backoff 演算法,這能避免 livelock 問題,但如果系統負載高時,它仍有可能將高優先順序的執行緒阻塞數十秒之久;另一種方案是使用 handoff lock 演算法,這也是 libobjc 目前正在使用的。鎖的持有者會把執行緒 ID 儲存到鎖內部,鎖的等待者會臨時貢獻出它的優先順序來避免優先順序反轉的問題。理論上這種模式會在比較複雜的多鎖條件下產生問題,但實踐上目前還一切都好。
OSSpinLock 自旋鎖,效能最高的鎖。原理很簡單,就是一直 do while 忙等。它的缺點是當等待時會消耗大量 CPU 資源,所以它不適用於較長時間的任務。對於記憶體快取的存取來說,它非常合適。
-摘自ibireme

所以說不建議再繼續使用,不過可以拿來玩耍一下,匯入標頭檔案#import <libkern/OSAtomic.h>

#import <libkern/OSAtomic.h>
@interface MYOSSpinLockViewController ()
{
    OSSpinLock spinlock;  //宣告pthread_mutex_t的結構
}
@end

@implementation MYOSSpinLockViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    spinlock = OS_SPINLOCK_INIT;
    /**
     *  初始化
     *
     */
}

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    /**
     *  加鎖
     */
    OSSpinLockLock(&spinlock);
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }
    /**
     *  解鎖
     */
    OSSpinLockUnlock(&spinlock);
}
@end

OSSpinLock的效能真的很卓越,可惜啦

9. GCD執行緒阻斷dispatch_barrier_async/dispatch_barrier_sync

dispatch_barrier_async/dispatch_barrier_sync在一定的基礎上也可以做執行緒同步,會線上程佇列中打斷其他執行緒執行當前任務,也就是說只有用在併發的執行緒佇列中才會有效,因為序列佇列本來就是一個一個的執行的,你打斷執行一個和插入一個是一樣的效果。兩個的區別是是否等待任務執行完成。

注意:如果在當前執行緒呼叫dispatch_barrier_sync打斷會發生死鎖。

@interface MYdispatch_barrier_syncViewController ()
{
        __block double then, now;
}
@end

@implementation MYdispatch_barrier_syncViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}
- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }else{
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    }
}

- (void)getImageNameWithMultiThread{
    NSMutableArray *imageNames = [NSMutableArray new];
    int count = 1024*11;
    for (int i=0; i<count; i++) {
        [imageNames addObject:[NSString stringWithFormat:@"%d",i]];
    }
    then = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<count+1; i++) {
        //100來測試鎖有沒有正確的執行
        dispatch_barrier_async(self.synchronizationQueue, ^{
             [self getIamgeName:imageNames];
        });
    }
}


參考
iOS 開發中的八種鎖(Lock)
iOS多執行緒-各種執行緒鎖的簡單介紹
ibireme的不再安全的 OSSpinLock

相關文章