OC底層原理 - 23 iOS中的鎖

引言

鎖是開發中最常用的同步工具,通過鎖來實現對臨界資源的訪問控制,從而使目標代碼段同一時間只會被一個線程執行。這是一種以犧牲性能為代價的方法。

鎖的實現依賴于原子操作,不同的處理器(intel、arm),不同的架構(單核、多核)實現原子操作的方式不一樣。有的是通過加鎖封鎖總線,有的是做成單指令,有的是依據標志位,有的是依據CPU相關的指令對,總之,不同的機制可以實現原子操作。

原子操作,就像原子一樣不可再分割的操作,即:一個操作(有可能包含多個子操作)只要開始執行,在執行完畢前,不會被其它操作或者指令中斷。原子操作解決了多線程不安全問題中的原子性問題。如果沒有原子操作的話,操作可能會因為中斷異常等各種原因引起數據狀態的不一致,從而影響到程序的正確性。

iOS中的atomic屬性修飾符的語義就是原子操作。被atomic所修飾的屬性,確保了setter和getter的原子性,這使得setter和getter這兩個方法是線程安全的,但是對于整個對象來說,不一定是線程安全的。并且atomic比nonatomic開銷要大很多,所以一般考慮到性能時,會將屬性修飾符設置為nonatomic。

雖然鎖是同步兩個線程的有效辦法,但是獲取鎖是一個相對昂貴的操作,即使在無爭用的情況下,也是如此。相比之下,許多原子操作只需要花費一小部分時間就可以完成,并且可以像鎖一樣有效。

使用鎖可以保證多線程操作共享數據時的安全問題,卻也降低了程序的執行效率。鎖的這種機制無法徹底避免以下幾點問題:
① 鎖引起的線程阻塞,對于沒有能占用到鎖的線程或者進程將會一直等待鎖的占有者釋放資源后才能繼續。
② 申請和釋放鎖的操作增加了很多訪問共享資源的消耗。
③ 鎖不能很好的避免編程開發者設計實現的程序出現死鎖或者活鎖的可能。
④ 優先級反轉和鎖護送怪現象。
⑤ 難以調試。

鎖的分類

鎖的分類多種多樣,根據線程的狀態可以分為:互斥鎖自旋鎖

互斥鎖:互斥鎖充當資源周圍的保護屏障,如果多個線程競爭同一個互斥鎖,每次只允許一個線程訪問。如果一個互斥鎖正在使用中,另一個線程試圖獲取它,該線程就會阻塞,進入睡眠狀態,直到該互斥鎖被它的持有者釋放后再將其喚醒。注意:互斥鎖阻塞的線程處于休眠狀態

自旋鎖:如果一個自旋鎖正在使用中,另一個線程試圖獲取它時,該線程不會進入睡眠狀態,而是反復輪詢其鎖條件,直到該條件為真。這適用于競爭預期較低的情況。注意:自旋鎖阻塞的線程處于忙等狀態

使線程進入睡眠狀態,主動讓出時間片并不代表效率高,因為操作系統切換到另一個線程上下文時,通常需要10ms,而且需要切換兩次。因此,如果鎖的預期等待時間很短,輪詢通常比線程休眠更有效。

iOS中的鎖

轉載自iOS多線程編程(七) 同步機制與鎖

pthread_mutex 互斥鎖

互斥鎖是一種用來防止多個線程同一時刻對共享資源進行訪問的信號量,它的原子性確保了如果一個線程鎖定了一個互斥量,將沒有其他線程在同一時間可以鎖定這個互斥量。它的唯一性確保了只有它解鎖了這個互斥量,其他線程才可以對其進行鎖定。當一個線程鎖定一個資源的時候,其他對該資源進行訪問的線程將會被掛起,直到該線程解鎖了互斥量,其他線程才會被喚醒,進一步才能鎖定該資源進行操作。

pthread_mutex是POSIX提供的互斥鎖,基于C語言實現,可跨平臺。基本上OC層面的互斥鎖都是基于pthread_mutex實現的。主要的函數如下:

// 宏定義。用于靜態的mutex的初始化,采用默認的attr。
PTHREAD_MUTEX_INITIALIZER 
// 用于動態的mutex的初始化,第二個參數為mutex的屬性attr
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 
// 請求獲得鎖,如果當前mutex未被持有,則加鎖成功;
// 如果當前mutex已被持有,那么請求加鎖線程不會獲得成功,并阻塞線程,直到mutex被釋放
int pthread_mutex_lock(pthread_mutex_t *mutex); 
// 釋放鎖
int pthread_mutex_unlock(pthread_mutex_t *mutex); 
// 嘗試獲得鎖,如果當前mutex已經被持有或者不可用,這個函數就直接return,不會阻塞當前線程
int pthread_mutex_trylock(pthread_mutex_t *mutex); 
// 銷毀mutex鎖,并且釋放所有它所占有的資源
int pthread_mutex_destroy(pthread_mutex_t *mutex); 

使用pthread_mutex的主要過程為:

  • ① 創建pthread_mutex;
  • ② 使用pthread_mutex_lock加鎖,使用pthread_mutex_unlock解鎖;
  • ③ 銷毀pthread_mutex;

創建pthread_mutex:

初始化pthread_mutex有兩種方式,一種是通過宏定義(PTHREAD_MUTEX_INITIALIZER)獲得默認的互斥鎖,另一種是通過函數(pthread_mutex_init )創建鎖。如果不需要自定義pthread_mutex的屬性信息,使用宏定義的方式更快速便捷。

使用pthread_mutex_lock加鎖與pthread_mutex_unlock解鎖

pthread_mutex(互斥鎖)利用排他性來保證線程安全,在同一時刻只允許一個線程獲得鎖。如果一個線程已經獲得互斥鎖,另一個線程就無法訪問,直到鎖的持有者正確的釋放了互斥鎖,另一個線程才有機會獲得鎖。

- (void)pthread_mutexDemo {
  
    // 創建mutex
    __block pthread_mutex_t mutex = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
    // 線程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        pthread_mutex_lock(&mutex);
        NSLog(@"執行任務A---%@",[NSThread currentThread]);
        sleep(5);
        NSLog(@"任務A執行完畢---%@",[NSThread currentThread]);
        pthread_mutex_unlock(&mutex);
    });
    // 線程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);// 讓線程1的任務先執行
        pthread_mutex_lock(&mutex);
        NSLog(@"執行任務B---%@",[NSThread currentThread]);
        pthread_mutex_unlock(&mutex);
    });
    
    // 銷毀mutex:確保mutex使用完畢再銷毀
//    pthread_mutex_destroy(&mutex);
}

// 打印結果:
2021-02-27 21:00:14.034887+0800 lockDemo[83241:6444414] 執行任務A---<NSThread: 0x600000fad000>{number = 5, name = (null)}
2021-02-27 21:00:19.039718+0800 lockDemo[83241:6444414] 任務A執行完畢---<NSThread: 0x600000fad000>{number = 5, name = (null)}
2021-02-27 21:00:19.040232+0800 lockDemo[83241:6444416] 執行任務B---<NSThread: 0x600000faee80>{number = 3, name = (null)}

本例中,線程1先獲得互斥鎖,盡管線程2的異步任務在sleep(1)后就可執行,但此時線程1已持有互斥鎖,所以再次遇到pthread_mutex_lock(&mutex)時,必須等待,此時線程2處于阻塞態,直到 sleep(5)后線程1釋放互斥鎖,線程2才被喚醒繼續執行任務。

使用pthread_mutex_trylock

除了pthread_mutex_lock函數外,pthread_mutex還提供了pthread_mutex_trylock 函數,與pthread_mutex_lock不同的是,使用pthread_mutex_trylock 函數來申請加鎖,不管是否能獲得鎖都立即返回,并不阻塞線程。如果申請失敗則返回錯誤:EBUSY(鎖尚未解除)或者EINVAL(鎖變量不可用)。一旦在trylock的時候有錯誤返回,那就把前面已經拿到的鎖全部釋放,然后過一段時間再來一遍。

如果將上例中線程2的pthread_mutex_lock(&mutex)操作,換成pthread_mutex_trylock(&mutex)。則結果為

2021-02-27 21:04:54.976015+0800 lockDemo[62208:9380951] 執行任務A---<NSThread: 0x6000017de040>{number = 6, name = (null)}
2021-02-27 21:04:55.977173+0800 lockDemo[62208:9380952] 執行任務B---<NSThread: 0x6000017a5980>{number = 4, name = (null)}
2021-02-27 21:04:59.980902+0800 lockDemo[62208:9380951] 任務A執行完畢---<NSThread: 0x6000017de040>{number = 6, name = (null)}

注意事項

  • 避免多次申請鎖或釋放未獲得的鎖

使用pthread_mutex時,pthread_mutex_lock與pthread_mutex_unlock要成對使用,一般情況下,一個線程只能申請一次鎖,也只能在獲得鎖的情況下才能釋放鎖,多次申請鎖或釋放未獲得的鎖都會導致異常。一定要確保在正確的時機獲得鎖和釋放鎖。

  • 避免阻塞

假設在已經獲得鎖的情況下再次申請鎖,線程會因為等待鎖的釋放而進入睡眠狀態,同時也不可能釋放鎖。

  • 避免死鎖

如果兩個線程存在互相等待釋放鎖的情況,也會導致死鎖的發生。

  • 記得pthread_mutex_destroy銷毀鎖,但要確保pthread_mutex已使用完畢。

pthread_mutex(recursive) 遞歸鎖

在實際開發中,有可能存在這樣的需求,遞歸調用或需要重復的獲得鎖。這種情況下,如果使用pthread_mutex(互斥鎖)就會阻塞線程,任務也就無法繼續執行。這就需要使用遞歸鎖來解決問題了。

遞歸鎖是互斥鎖的變體。遞歸鎖允許單個線程在釋放鎖之前多次獲取該鎖(可重入,保存了鎖的次數信息)。而不會阻塞當前線程,其他線程仍然處于阻塞狀態,直到鎖的持有者以獲得鎖的相同次數釋放鎖。

遞歸鎖主要在遞歸迭代期間使用,也可以在多個方法分別需要獲得鎖的情況下使用。

遞歸鎖的使用:

pthread_mutex維護了以下幾種鎖類型:

/*
 * Mutex type attributes
 */
#define PTHREAD_MUTEX_NORMAL        0     // 普通互斥鎖
#define PTHREAD_MUTEX_ERRORCHECK    1     // 檢查鎖
#define PTHREAD_MUTEX_RECURSIVE     2     // 遞歸鎖
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL

PTHREAD_MUTEX_NORMAL是默認屬性的互斥鎖;與PTHREAD_MUTEX_DEFAULT等同。
PTHREAD_MUTEX_ERRORCHECK 查錯鎖:以損失些許性能的方式返回錯誤信息;
PTHREAD_MUTEX_RECURSIVE就是遞歸鎖;

可以通過pthread_mutexattr_t屬性設置鎖的類型,示例代碼如下:

- (void)pthread_mutex_recursiveDemo {
    
    // init attr
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
//    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);

    // init mutex
    __block pthread_mutex_t mutex_recursive;
    pthread_mutex_init(&mutex_recursive, &attr);
    pthread_mutexattr_destroy(&attr);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
        static void (^RecursiveMethod)(int);
        RecursiveMethod = ^(int value) {
            // lock
            pthread_mutex_lock(&mutex_recursive);
            if (value > 0) {
                NSLog(@"value = %d,thread = %@",value,[NSThread currentThread]);
                RecursiveMethod(value - 1);
            }else{
                pthread_mutex_destroy(&mutex_recursive);
            }
            // unlock
            pthread_mutex_unlock(&mutex_recursive);
        };
        
        RecursiveMethod(5);
    });
    //    使用完畢后,銷毀
    //    pthread_mutex_destroy(& mutex_recursive);
}

// 打印結果:
2021-02-27 21:18:33.418542+0800 lockDemo[83366:6460107] value = 5,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}
2021-02-27 21:18:33.418716+0800 lockDemo[83366:6460107] value = 4,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}
2021-02-27 21:18:33.418845+0800 lockDemo[83366:6460107] value = 3,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}
2021-02-27 21:18:33.419116+0800 lockDemo[83366:6460107] value = 2,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}
2021-02-27 21:18:33.419250+0800 lockDemo[83366:6460107] value = 1,thread = <NSThread: 0x6000037bd380>{number = 6, name = (null)}

注意: pthread_mutex(recursive)只保證在單線程情況下可重入,當多個線程獲取相同的pthread_mutex(recursive)鎖會導致死鎖的發生。

pthread_rwlock(讀寫鎖)

基本上所有的問題都可以用互斥的方案去解決,但是可以解決并不代表適合。

pthread_mutex(互斥鎖)有個缺點,就是只要鎖住了,不管其他線程要干什么,都不允許進入臨界區。設想這樣一種情況:臨界區變量a正在被線程1讀取,加了個mutex鎖,線程2如果也要讀變量a,因為被線程1加了個互斥鎖,就只能等待線程1讀取完畢。但事實情況是,讀取數據并不影響數據內容本身,所以即便被1個線程讀著,另外一個線程也應該被允許去讀。除非另外一個線程是寫操作,為了避免數據不一致的問題,寫線程就需要等讀線程都結束了再寫。

因此誕生了讀寫鎖,有的地方也叫共享-獨占鎖

讀寫鎖的特性是這樣的,當一個線程加了讀鎖訪問臨界區,另外一個線程也想訪問臨界區讀取數據的時候,也可以加一個讀鎖,這樣另外一個線程就能夠成功進入臨界區進行讀操作了。此時讀鎖線程有兩個。當第三個線程需要進行寫操作時,它需要加一個寫鎖,這個寫鎖只有在讀鎖的擁有者為0時才有效。也就是等前兩個讀線程都釋放讀鎖之后,第三個線程就能進去寫了。總結一下就是:

  • 當讀寫鎖被一個線程以讀模式占用的時候,寫操作的其他線程會被阻塞,讀操作的其他線程還可以繼續進行。
  • 當讀寫鎖被一個線程以寫模式占用的時候,寫操作的其他線程會被阻塞,讀操作的其他線程也被阻塞。

這樣更精細的控制,就能減少mutex導致的阻塞延遲時間。如果受保護的數據結構經常被讀取,并且只偶爾修改,則可以顯著提高性能。雖然用mutex也能起作用,但這種場合,明顯讀寫鎖更好。

pthread中讀寫鎖主要函數如下:

// 靜態初始化方法
PTHREAD_RWLOCK_INITIALIZER
// 動態初始化,可傳pthread_rwlockattr_t屬性
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
// 銷毀 pthread_rwlock
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 獲得讀鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 嘗試獲得讀鎖
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
// 獲得寫鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 嘗試獲得寫鎖
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 釋放鎖
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

使用讀寫鎖與pthread_mutex類似,都是通過初始化創建鎖,之后根據讀寫不同場景進行加鎖、解鎖操作,在使用完畢后別忘了銷毀鎖。示例代碼如下:

- (void)pthread_rwlock_demo {
    pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
    _rwlock = rwlock;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 讀
        [self readWithTag:1];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 讀
        [self readWithTag:2];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 寫
        [self writeWithTag:3];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 寫
        [self writeWithTag:4];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 讀
        [self readWithTag:5];
    });
    //使用完畢后銷毀鎖:不可在未使用完畢前銷毀
    //pthread_rwlock_destroy(&_rwlock);
}

- (void)readWithTag:(NSInteger )tag {
    pthread_rwlock_rdlock(&_rwlock);
    NSLog(@"start read ---- %ld",tag);
    self.path = [[NSBundle mainBundle] pathForResource:@"pthread_rwlock" ofType:@".txt"];
    self.content = [NSString stringWithContentsOfFile:self.path encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"end   read ---- %ld",tag);
    pthread_rwlock_unlock(&_rwlock);
}

- (void) writeWithTag:(NSInteger)tag {
    pthread_rwlock_wrlock(&_rwlock);
    NSLog(@"start wirte ---- %ld",tag);
    [self.content writeToFile:self.path atomically:YES encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"end   wirte ---- %ld",tag);
    pthread_rwlock_unlock(&_rwlock);
}

// 打印結果 :  讀操作可共享,寫操作互斥
2021-02-27 21:29:44.081500+0800 lockDemo[82462:10201536] start read ---- 2
2021-02-27 21:29:44.081500+0800 lockDemo[82462:10201541] start read ---- 1
2021-02-27 21:29:44.081795+0800 lockDemo[82462:10201536] end   read ---- 2
2021-02-27 21:29:44.081795+0800 lockDemo[82462:10201541] end   read ---- 1
2021-02-27 21:29:44.082017+0800 lockDemo[82462:10201537] start wirte ---- 3
2021-02-27 21:29:44.082182+0800 lockDemo[82462:10201537] end   wirte ---- 3
2021-02-27 21:29:44.082351+0800 lockDemo[82462:10201535] start wirte ---- 4
2021-02-27 21:29:44.082459+0800 lockDemo[82462:10201535] end   wirte ---- 4
2021-02-27 21:29:44.082617+0800 lockDemo[82462:10201538] start read ---- 5
2021-02-27 21:29:44.082799+0800 lockDemo[82462:10201538] end   read ---- 5

注意事項

避免寫線程饑餓
由于讀寫鎖的性質,在默認情況下是很容易出現寫線程饑餓的。因為它必須要等到所有讀鎖都釋放之后,才能成功申請寫鎖。比如在寫線程阻塞的時候,有很多讀線程是可以一個接一個地在那兒插隊的(在默認情況下,只要有讀鎖在,寫鎖就無法申請,然而讀鎖可以一直申請成功,就導致所謂的插隊現象),那么寫線程就不知道什么時候才能申請成功寫鎖了,然后它就餓死了。所以要注意鎖建立后的優先級問題。不過不同系統的實現版本對寫線程的優先級實現不同。Solaris下面就是寫線程優先,其他系統默認讀線程優先。

pthread_cond (條件變量)

當我們在使用多線程的時候,有時一把只會lock和unlock的鎖未必就能完全滿足我們的使用。因為普通的鎖只能關心鎖與不鎖,而不在乎用什么鑰匙(滿足什么條件)才能開鎖,而我們在處理資源共享的時候,有時候需要只有滿足一定條件的情況下才能打開這把鎖。

這時候,POSIX提供的pthread_cond(條件變量)就派上了用場。主要的函數如下:

// 靜態初始化
PTHREAD_COND_INITIALIZER
// 動態初始化并允許設置屬性
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
// 銷毀條件變量
int pthread_cond_destroy(pthread_cond_t *cond);
// 發送信號(給指定線程)
int pthread_cond_signal(pthread_cond_t *cond);
// 廣播信號(給所有線程)
int pthread_cond_broadcast(pthread_cond_t *cond);
// 等待信號
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 等待信號,如果在指定時間仍未收到信號,則返回
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mute

條件變量可以做到讓一個線程等待多個線程的結束,并在合適的時候喚醒正在等待的線程,具體是什么時候,取決于你設置的條件是否滿足。

示例代碼如下:

pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean        ready_to_go = false;
 
void MyCondInitFunction()
{
    mutex =  (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
    pthread_cond_init(&condition, NULL);
}
 
void MyWaitOnConditionFunction()
{
    // Lock the mutex.
    pthread_mutex_lock(&mutex);
    // If the predicate is already set, then the while loop is bypassed;
    // otherwise, the thread sleeps until the predicate is set.
    while(ready_to_go == false)
    {
        pthread_cond_wait(&condition, &mutex);
    }
    
    // Do work. (The mutex should stay locked.)
   
    // Reset the predicate and release the mutex.
    ready_to_go = false;
    pthread_mutex_unlock(&mutex);
}

void SignalThreadUsingCondition()
{
    // At this point, there should be work for the other thread to do.
    pthread_mutex_lock(&mutex);
    
    ready_to_go = true;
    // Signal the other thread to begin work.
    pthread_cond_signal(&condition);
    pthread_mutex_unlock(&mutex);
}

- (void)pthread_cont_demo {
    MyCondInitFunction();
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        MyWaitOnConditionFunction();
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        SignalThreadUsingCondition();
    });
}

補充一下,原則上pthread_cond_signal是只通知一個線程,pthread_cond_broadcast是用于通知很多線程。但POSIX標準也允許讓pthread_cond_signal用于通知多個線程,不強制要求只允許通知一個線程。具體看各系統的實現。

另外,在調用pthread_cond_wait之前,必須要申請互斥鎖,當線程通過pthread_cond_wait進入waiting狀態時,會釋放傳入的互斥鎖。

NSLock (互斥鎖)

NSLockCocoa 基于pthread_mutex實現的一個基本的互斥鎖。對應pthread_mutex的PTHREAD_MUTEX_ERRORCHECK的類型。遵循NSLocking協議,該協議定義了lock和unlock方法。通過lockunlock來進行鎖定和解鎖。

實際上,OC層面的基于pthread_mutex封裝的鎖對象都遵循NSLocking協議,這樣設計的目的是因為,對于這些鎖的鎖定與解鎖行為對于底層的操作是一致的。使用這些方法來獲取和釋放鎖,就像使用任何pthread_mutex一樣。

除了NSLocking協議提供的標準鎖定行為,NSLock類還添加了tryLocklockBeforeDate:方法。

  • tryLock方法嘗試獲取該鎖,但如果該鎖不可用,并不會阻塞,該方法只返回NO。

  • lockBeforeDate:方法嘗試獲取鎖,但是如果在指定Date的時間限制內沒有獲得鎖,則會解除線程阻塞(并返回NO)。

使用示例如下:

- (void)nslock_demo {
    //主線程
    NSLock *lock = [[NSLock alloc] init];
    //線程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [lock lock];
        NSLog(@"線程1任務 開始");
        sleep(2);
        NSLog(@"線程1任務 結束");
        [lock unlock];
    });
    //線程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        if ([lock tryLock]) {//嘗試獲取鎖,如果獲取不到返回NO,不會阻塞該線程
            NSLog(@"線程2嘗試獲取鎖,鎖可用");
            [lock unlock];
        }else{
            NSLog(@"線程2嘗試獲取鎖,鎖不可用");
        }
        
        NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];
        if ([lock lockBeforeDate:date]) {//嘗試在未來的3s內獲取鎖,并阻塞該線程,如果3s內獲取不到恢復線程, 返回NO,不會阻塞該線程
            NSLog(@"沒有超時,線程2獲得鎖");
            [lock unlock];
        }else{
            NSLog(@"超時,線程2沒有獲得鎖");
        }
    });
}

// 打印結果:
2021-02-27 21:44:10.071157+0800 lockDemo[36464:983765] 線程1任務 開始
2021-02-27 21:44:11.074331+0800 lockDemo[36464:983761] 線程2嘗試獲取鎖,鎖不可用
2021-02-27 21:44:12.074832+0800 lockDemo[36464:983765] 線程1任務 結束
2021-02-27 21:44:12.075065+0800 lockDemo[36464:983761] 沒有超時,線程2獲得鎖

NSRecursiveLock (遞歸鎖)

NSRecursiveLock是Cocoa對pthread_mutex互斥鎖 PTHREAD_MUTEX_RECURSIVE類型的封裝。與pthread_mutex(遞歸鎖)一樣,主要是用在循環或遞歸操作中。該鎖可以被同一個線程多次獲取,而不會被阻塞。它記錄了成功獲得鎖的次數,每一次成功的獲得鎖,都必須有一個配套的釋放鎖與其對應,只有當所有的加鎖和解鎖調用都被平衡后,鎖才會被實際釋放,以便其他線程能夠獲取它。

除了實現NSLocking協議的方法外,NSRecursiveLock還提供了兩個方法,分別如下:

// 在給定的時間之前去嘗試請求一個鎖
- (BOOL)lockBeforeDate:(NSDate *)limit

// 嘗試去請求一個鎖,并會立即返回一個布爾值,表示嘗試是否成功
- (BOOL)tryLock

使用示例如下:

- (void)NSRecursiveLock_demo {
    //主線程
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
    //線程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void(^MyRecursiveFunction)(int);
        MyRecursiveFunction = ^(int value)
        {
            [recursiveLock lock];
            if (value > 0)
            {
                NSLog(@"遞歸任務1--%d",value);
                sleep(2);
                --value;
                MyRecursiveFunction(value);
            }
            [recursiveLock unlock];
        };
        MyRecursiveFunction(5);
    });
    //線程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        [recursiveLock lock];
        NSLog(@"任務2");
        [recursiveLock unlock];
    });
}

// 打印結果如下:
2021-02-27 21:48:33.853605+0800 lockDemo[83298:10293307] 遞歸任務1--5
2021-02-27 21:48:35.856179+0800 lockDemo[83298:10293307] 遞歸任務1--4
2021-02-27 21:48:37.859868+0800 lockDemo[83298:10293307] 遞歸任務1--3
2021-02-27 21:48:39.863572+0800 lockDemo[83298:10293307] 遞歸任務1--2
2021-02-27 21:48:41.868646+0800 lockDemo[83298:10293307] 遞歸任務1--1
2021-02-27 21:48:43.870858+0800 lockDemo[83298:10293303] 任務2

注意:由于遞歸鎖只有在所有鎖操作與解鎖操作得到平衡后才會被釋放,長時間持有任何鎖會導致其他線程阻塞,直到遞歸完成。如果可以通過重寫代碼來消除遞歸,或者消除使用遞歸鎖的需要,那么可能會獲得更好的性能。

NSCondition (條件)

NSCondition是對POSIX條件pthread_cond的封裝, 它將所需的鎖和條件數據結構包裝在一個對象中。使得開發者可以像鎖定互斥鎖一樣鎖定它,然后像等待條件一樣等待它。

NSConditionNSLock、@synchronized等是不同的是,NSCondition可以給每個線程分別加鎖,加鎖后不影響其他線程進入臨界區。其它線程也能上鎖,而之后可以根據條件決定是否繼續運行線程,即線程是否要進入 waiting 狀態.

除了實現NSLocking協議的方法外,NSCondition還提供了以下函數:

- (void)wait;   // 等待信號
- (BOOL)waitUntilDate:(NSDate *)limit;  // 等待信號,如果limit時間已到,則直接返回
- (void)signal; // 發送信號
- (void)broadcast; // 廣播信號

通過NSCondition可以實現不同線程的調度。一個線程被某一個條件所阻塞,直到另一個線程滿足該條件從而發送信號給該線程使得該線程可以正確的執行。

- (void)NSCondition_demo {
    __block NSInteger timeToDoWork = 0;
    NSCondition *cocoaCondition = [[NSCondition alloc] init];
    // 線程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [cocoaCondition lock];
        while (timeToDoWork <= 0){
            [cocoaCondition wait];
        }
         
        timeToDoWork--;
         
        // Do real work here.
         
        [cocoaCondition unlock];
    });
    
    // 線程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);
        [cocoaCondition lock];
        timeToDoWork++;
        [cocoaCondition signal];
        [cocoaCondition unlock];
    });
}

NSConditionLock (條件鎖)

NSConditionLock是對NSCondition的進一步封裝,條件鎖對象所定義的互斥鎖可以用特定的值(某個條件)鎖定和解鎖。除了NSLocking協議外,NSConditionLock還提供如下函數與屬性:

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

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;  // 當condition的值滿足條件時 獲取鎖
- (BOOL)tryLock; // 嘗試獲得鎖,不管是否獲得成功都立即返回,不阻塞線程
- (BOOL)tryLockWhenCondition:(NSInteger)condition; // 當condition的值滿足條件時,嘗試加鎖
- (void)unlockWithCondition:(NSInteger)condition; // 釋放鎖,并將condition的值修改為執行值
- (BOOL)lockBeforeDate:(NSDate *)limit; // 在指定時間限制內獲取鎖,獲取失敗,返回NO
// 在指定時間內,當condition的值滿足條件時獲取鎖
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

通常,當線程需要以特定的順序執行任務時,比如當一個線程生產數據另一個線程消耗數據時,可以使用NSConditionLock對象。在生產者執行時,可以通過特定的條件獲得鎖(條件本身只是定義的一個整數值),當生產者完成時,它將解鎖,并將鎖的條件設置為可以喚醒消費者線程的條件。

下面的示例展示了如何使用條件鎖處理生產者-消費者問題。假設一個應用程序包含一個數據隊列。生產者線程向隊列中添加數據,消費者線程從隊列中提取數據。生成器不需要等待特定的條件,但是它必須等待鎖可用,這樣它才能安全地將數據添加到隊列中。

NSMutableArray *products = [NSMutableArray array];
NSConditionLock *lock = [[NSConditionLock alloc] init];
NSInteger HAS_DATA = 1;
NSInteger NO_DATA = 0;
    
dispatch_async(dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_DEFAULT), ^{
       
    while (1) {
        [lock lockWhenCondition:NO_DATA];
        [products addObject:[[NSObject alloc] init]];
        NSLog(@"produce a product, 總量: %zi", products.count);
        [lock unlockWithCondition:HAS_DATA];
        sleep(1);
    }
});
    
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
    while (1) {
        NSLog(@"wait for product");
        [lock lockWhenCondition:HAS_DATA];
        [products removeObjectAtIndex:0];
        NSLog(@"custome a product");
        [lock unlockWithCondition:NO_DATA];
    }
});

當生產者釋放鎖的時候,把條件設置成了1。這樣消費者可以獲得該鎖,進而執行程序,如果消費者獲得鎖的條件和生產者釋放鎖時給定的條件不一致,則消費者永遠無法獲得鎖,也不能執行程序。同樣,如果消費者釋放鎖給定的條件和生產者獲得鎖給定的條件不一致的話,則生產者也無法獲得鎖,程序也不能執行。

注意

  1. unlock 與 unlockWithCondition:(NSInteger)condition 的區別:
    • unlock:釋放鎖但并不改變condition的值;
    • unlockWithCondition:釋放鎖,并將condition的值修改為指定值。
  2. 由于在實現操作系統時的細微參與,即使代碼里沒有實際發出信號,條件鎖也允許以虛假的成功返回。為了避免由這些假信號引起的問題,您應該始終將謂詞與條件鎖結合使用。謂詞是確定線程繼續執行是否安全的更具體的方法。這個條件只是讓線程處于休眠狀態,直到發送信號的線程可以設置謂詞。

@sychronized

@sychronized是使用起來最簡單的互斥鎖,通常只需要@sychronized(obj)這樣一個簡單的指令就可以實現加/解鎖操作。

- (void)sychronized_demo {
    NSObject *obj = [[NSObject alloc] init];
    NSObject *obj1 = [[NSObject alloc] init];
    //線程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized(obj){
            NSLog(@"任務1");
            sleep(5);
        }
    });
    //線程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized(obj){
            NSLog(@"任務2");
        }
    });
}
// 打印結果:
2021-02-27 22:08:25.288126+0800 lockDemo[83702:10333558] 任務1
2021-02-27 22:08:30.291985+0800 lockDemo[83702:10333557] 任務2

@synchronized指令使用傳入的對象(obj)作為該鎖的唯一標識,只有當標識相同時,才為滿足互斥,如果線程2中的@synchronized(obj)改為@synchronized(obj1),線程2就不會被阻塞。

// 如果將線程2的 @synchronized(obj)換成 @synchronized(obj1),則
2021-02-27 22:09:42.831004+0800 lockDemo[83783:10344549] 任務1
2021-02-27 22:09:42.831014+0800 lockDemo[83783:10344546] 任務2

同時@synchronized還允許重入,前面提到的pthread_mutex(遞歸鎖)和NSRecursiveLock也支持重入,但它們只允許在同一線程內多次重入,而@synchronized支持多線程重入。這是因為@sychronized內部,除了維護了同一線程的加鎖次數lockCount外,還維護了使用唯一標識的線程數threadCount

@synchronized指令實現鎖的優點就是我們不需要在代碼中顯式的創建鎖對象,便可以實現鎖的機制,但作為一種預防措施,@synchronized塊會隱式的添加一個異常處理例程來保護代碼,該處理例程會在異常拋出的時候自動的釋放互斥鎖。所以如果不想讓隱式的異常處理例程帶來額外的開銷,你可以考慮使用鎖對象。

注意:確保傳入@synchronized的obj不為nil,因為如果傳入的obj為nil的話,實際上并不會做任何實際的內容,也無法達到加鎖的目的。

dispatch_semaphore信號量

dispatch_semaphoreNSCondition類似,都是一種基于信號的同步方式,但NSCondition信號只能發送,不能保存(如果沒有線程在等待,則發送的信號會失效)。而 dispatch_semaphore能保存發送的信號。dispatch_semaphore 的核心是 dispatch_semaphore_t 類型的信號量。

dispatch_semaphore是信號量,但當信號總量設為 1 時也可以當作鎖來。在沒有等待情況出現時,它的性能比 pthread_mutex 還要高,但一旦有等待情況出現時,性能就會下降許多。相對于 OSSpinLock 來說,它的優勢在于等待時不會消耗 CPU 資源。

與其相關的主要有三個函數:

  • dispatch_semaphore_t dispatch_semaphore_create(long value)
    輸出一個dispatch_semaphore_t類型且值為value的信號量。值得注意的是,這里的傳入的參數value必須大于或等于0,否則dispatch_semaphore_create會返回NULL。

  • long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
    這個函數會使傳入的信號量dsema的值加1;

  • long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
    這個函數會使傳入的信號量dsema的值減1;
    這個函數的作用是這樣的,如果dsema信號量的值大于0,該函數所處線程就繼續執行下面的語句,并且將信號量的值減1;如果desema的值為0,那么這個函數就阻塞當前線程等待timeout(注意timeout的類型為dispatch_time_t,不能直接傳入整形或float型數),如果等待的期間desema的值被dispatch_semaphore_signal函數加1了,且該函數(即dispatch_semaphore_wait)所處線程獲得了信號量,那么就繼續向下執行并將信號量減1。如果等待期間沒有獲取到信號量或者信號量的值一直為0,那么等到timeout時,其所處線程自動執行其后語句。

示例代碼如下:

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);
            NSLog(@"需要線程同步的操作1 開始");
            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);
    });
    
//執行結果為:
需要線程同步的操作1 開始
需要線程同步的操作1 結束
需要線程同步的操作2

OSSpinLock 自旋鎖

OSSpinLock是一把自旋鎖,性能很高。因為它一直是do while忙等狀態。這種自旋鎖的缺點是當等待時會消耗大量CPU資源,所以它不適用于較長時間的任務。

OSSpinLock是整數類型,約定是解鎖為零,鎖定為非零。鎖必須自然對齊,并且不能在緩存抑制的內存中。

如果鎖已經被持有,OSSpinLockLock()將自旋,但會使用各種各樣的策略來后退,使其對大多數優先級反轉活鎖免疫。但因為它可以旋轉,所以在某些情況下可能效率低下。

如果鎖被持有,OSSpinLockTry()立即返回false,如果它獲得了鎖,則返回true。它不自旋。 OSSpinLockUnlock()通過置零無條件地解鎖鎖。

OSSpinLock 示例

- (void)osspinlock_demo {
    __block OSSpinLock theLock = OS_SPINLOCK_INIT;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(&theLock);
        NSLog(@"線程1");
        sleep(5);
        OSSpinLockUnlock(&theLock);
        NSLog(@"線程1解鎖成功");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        OSSpinLockLock(&theLock);
        NSLog(@"線程2");
        OSSpinLockUnlock(&theLock);
    });
}
// 打印結果
2021-02-27 22:05:13.526 ThreadLockControlDemo[2856:316247] 線程1
2021-02-27 22:05:23.528 ThreadLockControlDemo[2856:316247] 線程1解鎖成功
2021-02-27 22:05:23.529 ThreadLockControlDemo[2856:316260] 線程2

OSSpinLock 問題

新版iOS中,系統維護了5個不同的線程優先級/QoS: background,utility,default,user-initiated,user-interactive。高優先級線程始終會在低優先級線程前執行,一個線程不會受到比它更低優先級線程的干擾。這種線程調度算法會產生潛在的優先級反轉問題,從而破壞了OSSpinLock。

具體來說,如果一個低優先級的線程獲得鎖并訪問共享資源,這時一個高優先級的線程也嘗試獲得這個鎖,它會處于OSSpinLock的忙等狀態從而占用大量 CPU。此時低優先級線程無法與高優先級線程爭奪 CPU 時間,從而導致任務遲遲完不成、無法釋放OSSpinLock。

所以從iOS10.0開始,蘋果棄用了OSSpinLock,并用os_unfair_lock進行替代。不過,os_unfair_lock的實現屬于互斥鎖,當鎖被占用的時候,線程處于阻塞狀態,而非忙等。

iOS中的鎖的性能

在iOS中,各種鎖的性能如下圖所示:


16239819199365.jpg

從圖中可以看出,在iOS中的鎖性能從高往底依次是:

  • OSSpinLock(自旋鎖)
  • dispatch_semaphone(信號量)
  • pthread_mutex(互斥鎖)
  • NSLock(互斥鎖)
  • NSCondition(條件鎖)
  • pthread_mutex(recursive 互斥遞歸鎖)
  • NSRecursiveLock(遞歸鎖)
  • NSConditionLock(條件鎖)
  • synchronized(互斥鎖)

性能總結:

  1. OSSpinLock自旋鎖由于安全性問題,在iOS10之后已經被廢棄,其底層的實現用os_unfair_lock替代
    • 使用OSSpinLock會處于忙等待狀態
    • 使用os_unfair_lock會處于休眠狀態
  2. atomic原子鎖自帶一把自旋鎖,只能保證setter、getter時的線程安全,在日常開發中使用更多的還是nonatomic修飾屬性
    • atomic:當屬性在調用setter、getter方法時,會加上自旋鎖OSSpinLock,用于保證同一時刻只能有一個線程調用屬性的讀或寫,避免了屬性讀寫不同步的問題。由于是底層編譯器自動生成的互斥鎖代碼,會導致效率相對較低
    • nonatomic:當屬性在調用setter、getter方法時,不會加上自旋鎖,即線程不安全。由于編譯器不會自動生成互斥鎖代碼,可以提高效率
  3. @synchronized在底層維護了一個哈希表進行線程data的存儲,通過鏈表表示可重入(即嵌套)的特性,雖然性能較低,但由于簡單好用,使用頻率很高
  4. NSLock、NSRecursiveLock底層是對pthread_mutex的封裝
  5. NSConditionNSConditionLock是條件鎖,底層都是對pthread_mutex的封裝,當滿足某一個條件時才能進行操作,和信號量dispatch_semaphore類似
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容