iOS底層原理探索—多線程的“鎖”

探索底層原理,積累從點滴做起。大家好,我是Mars。

往期回顧

iOS底層原理探索 — OC對象的本質
iOS底層原理探索 — class的本質
iOS底層原理探索 — KVO的本質
iOS底層原理探索 — KVC的本質
iOS底層原理探索 — Category的本質(一)
iOS底層原理探索 — Category的本質(二)
iOS底層原理探索 — 關聯對象的本質
iOS底層原理探索 — block的本質(一)
iOS底層原理探索 — block的本質(二)
iOS底層原理探索 — Runtime之isa的本質
iOS底層原理探索 — Runtime之class的本質
iOS底層原理探索 — Runtime之消息機制
iOS底層原理探索 — RunLoop的本質
iOS底層原理探索 — RunLoop的應用
iOS底層原理探索 — 多線程的本質
iOS底層原理探索 — 多線程的經典面試題

前言

多線程是iOS開發中很重要的一個環節,無論是開發過程還是在面試環節中,多線程出現的頻率都非常高。我們會通過幾篇文章的探索,深入淺出的分析多線程技術。

我們通過之前兩篇文章的分析,大致對多線程技術有了一定的了解,認識到了多線程技術的強大。但是多線程在應用過程中存在一定的安全隱患,今天我們繼續分析,來看多線程中如何解決這些問題。

多線程的安全隱患

當多個線程訪問同一塊資源時,很容易引發數據錯亂和數據安全問題。
比如多個線程訪問同一個對象、同一個變量、同一個文件。
具體場景為:存錢取錢、購買車票等。
我們模擬一下賣票的場景。一共有15張票,開設三個窗口賣票,每個窗口賣掉5張,我們來看一下多個線程訪問的時候會出現什么問題:

/** 賣1張票 */
- (void)saleTicket {
    int oldTicketsCount = self.ticketsCount;
    sleep(.2);
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    
    NSLog(@"還剩%d張票 - %@", oldTicketsCount, [NSThread currentThread]);
}

/** 模擬賣票 */
- (void)ticketTest {
    self.ticketsCount = 15;
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 窗口一
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    // 窗口二
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    // 窗口三
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

我們打印一下執行結果:


模擬賣票.png

我們發現,經過15次賣票后,正常來說15張票已經全部賣完才對,但是最后一次打印還剩5張票。這就出現了線程安全問題。

我們再來看下面一個例子:

多線程安全隱患.png

例如上圖中,一個integer類型的對象值為17,當線程A和線程B同時訪問到的時候,線程A做+1操作,同時線程B也做+1操作。

由于兩個線程訪問到integer類型對象時的值都為17,分別做+1操作后變量的值變為18。但是實際的結果應為做了兩次+1操作,值應該為19。這就出現了問題。

那么多線程同樣提供了解決方案:使用線程同步技術。

線程同步技術

常見的線程同步技術是加鎖
我們再講上面的例子優化:

加鎖.png

如上圖,我們應用了加鎖技術后,當線程A訪問和修改變量時,會加鎖,進行+1操作,此時變量值變為18,然后對其解鎖。解鎖后,當線程B訪問和修改變量時,此時變量的值已經為18,再進行+1操作,完成修改。這樣就保證了線程安全。

iOS中的線程同步方案

iOS中為我們提供了一下幾種線程同步方案:

  • OSSpinLock —— 自旋鎖
  • os_unfair_lock
  • pthread_mutex —— 互斥鎖
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • dispatch_semaphore —— 信號量
  • @synchronized

接下來我們逐一對以上方案進行分析:

1、OSSpinLock 自旋鎖

OSSpinLock叫做自旋鎖,等待鎖的線程會處于忙等(busy-wait)狀態,一直占用著CPU資源。目前已經不再安全,可能會出現優先級反轉問題。在iOS10版本以后就不再支持這一技術。
如果等待鎖的線程優先級較高,它會一直占用著CPU資源,優先級低的線程就無法釋放鎖
需要導入頭文件#import <libkern/OSAtomic.h>

//初始化鎖
OSSpinLock lock = OS_SPINLOCK_INIT; 
//嘗試加鎖(如果需要等待,就不嘗試加鎖,直接返回false,如果不需要等待就加鎖,返回true)
bool result = OSSpinLockTry(&_lock); 
// 加鎖
OSSpinLockLock(&_lock);
//解鎖
OSSpinLockUnlock(&_lock); 

我們使用自旋鎖對上面賣票的例子進行優化:

#import <libkern/OSAtomic.h>
@property (assign, nonatomic) OSSpinLock lock;

// 初始化鎖
self.lock = OS_SPINLOCK_INIT;

/** 賣1張票 */
- (void)saleTicket {
    // 加鎖
    OSSpinLockLock(&_lock);
    
    int oldTicketsCount = self.ticketsCount;
    sleep(.2);
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    
    NSLog(@"還剩%d張票 - %@", oldTicketsCount, [NSThread currentThread]);
    
    // 解鎖
    OSSpinLockUnlock(&_lock);
}
加鎖后賣票.png

通過打印可以看到,加鎖后線程安全,賣票結果也正確。

我們在使用OSSpinLock自旋鎖時,系統會報出警告,告訴我們不推薦使用OSSpinLockLock,在iOS 10.0中棄用。并推薦使用<os / lock.h>中的os_unfair_lock_lock()來代替。

2、os_unfair_lock

os_unfair_lock用于取代不安全的OSSpinLock,從iOS10開始才支持。從底層調用看,等待os_unfair_lock鎖的線程會處于休眠狀態,并非忙等
需要導入頭文件#import <os/lock.h>

//初始化
os_unfair_lock moneyLock = OS_UNFAIR_LOCK_INIT; 
// 嘗試加鎖
os_unfair_lock_trylock(&_ticketLock); 
// 加鎖
os_unfair_lock_lock(&_ticketLock);  
// 解鎖
os_unfair_lock_unlock(&_ticketLock);  

3、pthread_mutex 互斥鎖

mutex叫做互斥鎖,等待鎖的線程會處于休眠狀態
需要導入頭文件#import <pthread.h>

// 初始化屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化鎖
pthread_mutex_init(mutex, &attr);
// 嘗試加鎖
pthread_mutex_trylock(&_ticketMutex);
// 加鎖
pthread_mutex_lock(&_ticketMutex);
// 解鎖
pthread_mutex_unlock(&_ticketMutex);
// 銷毀屬性
pthread_mutexattr_destroy(&attr);

互斥鎖的type有以下幾種:

#define PTHREAD_MUTEX_NORMAL        0 普通鎖
#define PTHREAD_MUTEX_ERRORCHECK    1
#define PTHREAD_MUTEX_RECURSIVE     2  遞歸鎖
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL
pthread_mutex遞歸鎖
// 初始化鎖的屬性
pthread_mutexattr_t attr;
pthread_attr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化鎖
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);

pthread_mutex條件

// 初始化鎖
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
// 初始化條件
pthread_cond_t condition;
pthread_cond_init(&condition, NULL);
// 等待條件(進入休眠,放開mutex鎖;被喚醒后,會再次對mutex加鎖)
pthread_cond_wait(&condition, &mutex);
// 激活一個等待該條件的線程
pthread_cond_signal(&condition);
// 激活所有等待該條件的線程
pthread_cond_broadcast(&condition);
// 銷毀相關資源
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&condition);

4、NSLock

NSLock是對mutex普通鎖的封裝

// 初始化鎖
NSLock *lock = [[NSLock alloc] init];
// 嘗試加鎖
[lock tryLock];
// 指定Date之前嘗試加鎖
[lock lockBeforeDate:[NSDate date]];
// 加鎖
[lock lock];
// 解鎖
[lock unlock];

5、NSRecursiveLock

NSRecursiveLock是對mutex 遞歸鎖的封裝,APINSLock基本一致。

// 初始化鎖
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
// 嘗試加鎖
[lock tryLock];
// 指定Date之前嘗試加鎖
[lock lockBeforeDate:[NSDate date]];
// 加鎖
[lock lock];
// 解鎖
[lock unlock];

6、NSCondition

NSCondition是對mutex和條件的封裝

// 初始化鎖
NSCondition *condition = [[NSCondition alloc] init];
// 加鎖
[condition lock];
// 等待條件
[condition wait];
// Date之前等待條件
[condition waitUntilDate:[NSDate date]];
// 激活一個等待該條件的線程
[condition signal];
// 激活所有等待該條件的線程
[condition broadcast];
// 解鎖
[condition unlock];

7、NSConditionLock

NSConditionLock是對NSCondition的進一步封裝,可以設置具體的條件值。

// 初始化鎖
NSConditionLock *condition = [[NSConditionLock alloc] init];
NSConditionLock *condition = [[NSConditionLock alloc] initWithCondition:1];
// 加鎖 - 可設置條件值
[condition lockWhenCondition:1 beforeDate:[NSDate date]];
[condition lockBeforeDate:[NSDate date]];
[condition lockWhenCondition:1];
[condition lock];
[condition tryLock];
[condition tryLockWhenCondition:1];
// 解鎖
[condition unlockWithCondition:1];
[condition unlock];

8、dispatch_queue(DISPATCH_QUEUE_SERIAL)串行隊列

使用GCD的串行隊列,實現線程同步

dispatch_queue_t queue = dispatch_queue_create("lock_queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    // 需要執行的任務
});

9、dispatch_semaphore信號量

semaphore叫做信號量,信號量的初始值,可以用來控制線程并發訪問的最大數量。信號量的初始值為1,代表同時只允許1條線程訪問資源,保證線程同步。

// 信號量的初始值
int value = 1;
// 初始化信號量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
// 如果信號量的值 >0,就 -1,然后往下執行代碼
// 如果信號量的值 <=0,就會休眠等待,直到信號量的值變成 >0
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 信號量的值 +1
dispatch_semaphore_signal(semaphore);

10、@synchronized

@synchronized是對mutex 遞歸鎖的封裝。可以查看源碼:objc4中的objc-sync.mm文件。
@synchronized(obj)內部會生成obj對應的遞歸鎖,然后進行加鎖、解鎖操作。obj 可以是同一個實例對象,類對象,靜態變量

@synchronized(obj) {
    //需要執行的任務
}

iOS線程同步方案性能比較

以上幾種鎖的性能從高到低排序:

  • os_unfair_lock
  • OSSpinLock
  • dispatch_semaphore
  • pthread_mutex
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSCondition
  • pthread_mutex(recursive)
  • NSRecursiveLock
  • NSConditionLock
  • @synchronized

自旋鎖、互斥鎖比較

什么情況使用自旋鎖比較劃算?

  • 預計線程等待鎖的時間很短
  • 加鎖的代碼(臨界區)經常被調用,但競爭情況很少發生
  • CPU資源不緊張
  • 多核處理器

什么情況使用互斥鎖比較劃算?

  • 預計線程等待鎖的時間較長
  • 單核處理器
  • 臨界區有IO操作
  • 臨界區代碼復雜或者循環量大
  • 臨界區競爭非常激烈

我們簡單的介紹了一下多線程中的線程安全問題,以及解決這些問題的線程同步方案,篇幅有限,實際案例會再后續文章中為大家解讀。

更多技術知識請關注公眾號
iOS進階


iOS進階.jpg
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容