怎么理解iOS開發(fā)中的鎖

本篇內(nèi)容介紹了“怎么理解iOS開發(fā)中的鎖”的有關(guān)知識,在實(shí)際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

專注于為中小企業(yè)提供成都網(wǎng)站設(shè)計(jì)、成都網(wǎng)站建設(shè)服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)莒縣免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動了成百上千企業(yè)的穩(wěn)健成長,幫助中小企業(yè)通過網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。

OSSpinLock

上述文章中已經(jīng)介紹了 OSSpinLock 不再安全,主要原因發(fā)生在低優(yōu)先級線程拿到鎖時(shí),高優(yōu)先級線程進(jìn)入忙等(busy-wait)狀態(tài),消耗大量 CPU 時(shí)間,從而導(dǎo)致低優(yōu)先級線程拿不到 CPU 時(shí)間,也就無法完成任務(wù)并釋放鎖。這種問題被稱為優(yōu)先級反轉(zhuǎn)。

為什么忙等會導(dǎo)致低優(yōu)先級線程拿不到時(shí)間片?這還得從操作系統(tǒng)的線程調(diào)度說起。

現(xiàn)代操作系統(tǒng)在管理普通線程時(shí),通常采用時(shí)間片輪轉(zhuǎn)算法(Round Robin,簡稱 RR)。每個(gè)線程會被分配一段時(shí)間片(quantum),通常在 10-100 毫秒左右。當(dāng)線程用完屬于自己的時(shí)間片以后,就會被操作系統(tǒng)掛起,放入等待隊(duì)列中,直到下一次被分配時(shí)間片。

自旋鎖的實(shí)現(xiàn)原理

自旋鎖的目的是為了確保臨界區(qū)只有一個(gè)線程可以訪問,它的使用可以用下面這段偽代碼來描述:

do {       Acquire Lock         Critical section  // 臨界區(qū)     Release Lock         Reminder section // 不需要鎖保護(hù)的代碼 } 復(fù)制代碼

在 Acquire Lock 這一步,我們申請加鎖,目的是為了保護(hù)臨界區(qū)(Critical Section) 中的代碼不會被多個(gè)線程執(zhí)行。

自旋鎖的實(shí)現(xiàn)思路很簡單,理論上來說只要定義一個(gè)全局變量,用來表示鎖的可用情況即可,偽代碼如下:

bool lock = false; // 一開始沒有鎖上,任何線程都可以申請鎖   do {       while(lock); // 如果 lock 為 true 就一直死循環(huán),相當(dāng)于申請鎖     lock = true; // 掛上鎖,這樣別的線程就無法獲得鎖         Critical section  // 臨界區(qū)     lock = false; // 相當(dāng)于釋放鎖,這樣別的線程可以進(jìn)入臨界區(qū)         Reminder section // 不需要鎖保護(hù)的代碼         } 復(fù)制代碼

注釋寫得很清楚,就不再逐行分析了??上н@段代碼存在一個(gè)問題: 如果一開始有多個(gè)線程同時(shí)執(zhí)行 while 循環(huán),他們都不會在這里卡住,而是繼續(xù)執(zhí)行,這樣就無法保證鎖的可靠性了。解決思路也很簡單,只要確保申請鎖的過程是原子操作即可。

原子操作

狹義上的原子操作表示一條不可打斷的操作,也就是說線程在執(zhí)行操作過程中,不會被操作系統(tǒng)掛起,而是一定會執(zhí)行完。在單處理器環(huán)境下,一條匯編指令顯然是原子操作,因?yàn)橹袛嘁惨ㄟ^指令來實(shí)現(xiàn)。

然而在多處理器的情況下,能夠被多個(gè)處理器同時(shí)執(zhí)行的操作任然算不上原子操作。因此,真正的原子操作必須由硬件提供支持,比如 x86 平臺上如果在指令前面加上 “LOCK” 前綴,對應(yīng)的機(jī)器碼在執(zhí)行時(shí)會把總線鎖住,使得其他 CPU不能再執(zhí)行相同操作,從而從硬件層面確保了操作的原子性。

這些非常底層的概念無需完全掌握,我們只要知道上述申請鎖的過程,可以用一個(gè)原子性操作 test_and_set 來完成,它用偽代碼可以這樣表示:

bool test_and_set (bool *target) {       bool rv = *target;      *target = TRUE;      return rv; } 復(fù)制代碼

這段代碼的作用是把 target 的值設(shè)置為 1,并返回原來的值。當(dāng)然,在具體實(shí)現(xiàn)時(shí),它通過一個(gè)原子性的指令來完成。

自旋鎖的總結(jié)

至此,自旋鎖的實(shí)現(xiàn)原理就很清楚了:

bool lock = false; // 一開始沒有鎖上,任何線程都可以申請鎖   do {       while(test_and_set(&lock); // test_and_set 是一個(gè)原子操作         Critical section  // 臨界區(qū)     lock = false; // 相當(dāng)于釋放鎖,這樣別的線程可以進(jìn)入臨界區(qū)         Reminder section // 不需要鎖保護(hù)的代碼         } 復(fù)制代碼

如果臨界區(qū)的執(zhí)行時(shí)間過長,使用自旋鎖不是個(gè)好主意。之前我們介紹過時(shí)間片輪轉(zhuǎn)算法,線程在多種情況下會退出自己的時(shí)間片。其中一種是用完了時(shí)間片的時(shí)間,被操作系統(tǒng)強(qiáng)制搶占。除此以外,當(dāng)線程進(jìn)行 I/O 操作,或進(jìn)入睡眠狀態(tài)時(shí),都會主動讓出時(shí)間片。顯然在 while 循環(huán)中,線程處于忙等狀態(tài),白白浪費(fèi) CPU 時(shí)間,最終因?yàn)槌瑫r(shí)被操作系統(tǒng)搶占時(shí)間片。如果臨界區(qū)執(zhí)行時(shí)間較長,比如是文件讀寫,這種忙等是毫無必要的。

信號量

之前我在  介紹 GCD 底層實(shí)現(xiàn)的文章 中簡單描述了信號量 dispatch_semaphore_t 的實(shí)現(xiàn)原理,它最終會調(diào)用到 sem_wait 方法,這個(gè)方法在 glibc 中被實(shí)現(xiàn)如下:

int sem_wait (sem_t *sem) {     int *futex = (int *) sem;   if (atomic_decrement_if_positive (futex) > 0)     return 0;   int err = lll_futex_wait (futex, 0);     return -1; ) 復(fù)制代碼

首先會把信號量的值減一,并判斷是否大于零。如果大于零,說明不用等待,所以立刻返回。具體的等待操作在 lll_futex_wait 函數(shù)中實(shí)現(xiàn),lll 是 low level lock 的簡稱。這個(gè)函數(shù)通過匯編代碼實(shí)現(xiàn),調(diào)用到 SYS_futex 這個(gè)系統(tǒng)調(diào)用,使線程進(jìn)入睡眠狀態(tài),主動讓出時(shí)間片,這個(gè)函數(shù)在互斥鎖的實(shí)現(xiàn)中,也有可能被用到。

主動讓出時(shí)間片并不總是代表效率高。讓出時(shí)間片會導(dǎo)致操作系統(tǒng)切換到另一個(gè)線程,這種上下文切換通常需要 10 微秒左右,而且至少需要兩次切換。如果等待時(shí)間很短,比如只有幾個(gè)微秒,忙等就比線程睡眠更高效。

可以看到,自旋鎖和信號量的實(shí)現(xiàn)都非常簡單,這也是兩者的加解鎖耗時(shí)分別排在第一和第二的原因。再次強(qiáng)調(diào),加解鎖耗時(shí)不能準(zhǔn)確反應(yīng)出鎖的效率(比如時(shí)間片切換就無法發(fā)生),它只能從一定程度上衡量鎖的實(shí)現(xiàn)復(fù)雜程度。

pthread_mutex

pthread 表示 POSIX thread,定義了一組跨平臺的線程相關(guān)的 API,pthread_mutex 表示互斥鎖。互斥鎖的實(shí)現(xiàn)原理與信號量非常相似,不是使用忙等,而是阻塞線程并睡眠,需要進(jìn)行上下文切換。

互斥鎖的常見用法如下:

pthread_mutexattr_t attr;   pthread_mutexattr_init(&attr);   pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定義鎖的屬性 pthread_mutex_t mutex;   pthread_mutex_init(&mutex, &attr) // 創(chuàng)建鎖 pthread_mutex_lock(&mutex); // 申請鎖       // 臨界區(qū) pthread_mutex_unlock(&mutex); // 釋放鎖   復(fù)制代碼

對于 pthread_mutex 來說,它的用法和之前沒有太大的改變,比較重要的是鎖的類型,可以有 PTHREAD_MUTEX_NORMALPTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE 等等,具體的特性就不做解釋了,網(wǎng)上有很多相關(guān)資料。

一般情況下,一個(gè)線程只能申請一次鎖,也只能在獲得鎖的情況下才能釋放鎖,多次申請鎖或釋放未獲得的鎖都會導(dǎo)致崩潰。假設(shè)在已經(jīng)獲得鎖的情況下再次申請鎖,線程會因?yàn)榈却i的釋放而進(jìn)入睡眠狀態(tài),因此就不可能再釋放鎖,從而導(dǎo)致死鎖。

然而這種情況經(jīng)常會發(fā)生,比如某個(gè)函數(shù)申請了鎖,在臨界區(qū)內(nèi)又遞歸調(diào)用了自己。辛運(yùn)的是 pthread_mutex 支持遞歸鎖,也就是允許一個(gè)線程遞歸的申請鎖,只要把 attr 的類型改成 PTHREAD_MUTEX_RECURSIVE 即可。

互斥鎖的實(shí)現(xiàn)

互斥鎖在申請鎖時(shí),調(diào)用了 pthread_mutex_lock 方法,它在不同的系統(tǒng)上實(shí)現(xiàn)各有不同,有時(shí)候它的內(nèi)部是使用信號量來實(shí)現(xiàn),即使不用信號量,也會調(diào)用到 lll_futex_wait 函數(shù),從而導(dǎo)致線程休眠。

上文說到如果臨界區(qū)很短,忙等的效率也許更高,所以在有些版本的實(shí)現(xiàn)中,會首先嘗試一定次數(shù)(比如 1000 次)的 testandtest,這樣可以在錯(cuò)誤使用互斥鎖時(shí)提高性能。

另外,由于 pthread_mutex 有多種類型,可以支持遞歸鎖等,因此在申請加鎖時(shí),需要對鎖的類型加以判斷,這也就是為什么它和信號量的實(shí)現(xiàn)類似,但效率略低的原因。

NSLock

NSLock 是 Objective-C 以對象的形式暴露給開發(fā)者的一種鎖,它的實(shí)現(xiàn)非常簡單,通過宏,定義了 lock方法:

#define    MLOCK \n- (void) lock\n{\n  int err = pthread_mutex_lock(&_mutex);\n  // 錯(cuò)誤處理 …… } 復(fù)制代碼

NSLock 只是在內(nèi)部封裝了一個(gè) pthread_mutex,屬性為 PTHREAD_MUTEX_ERRORCHECK,它會損失一定性能換來錯(cuò)誤提示。

這里使用宏定義的原因是,OC 內(nèi)部還有其他幾種鎖,他們的 lock 方法都是一模一樣,僅僅是內(nèi)部 pthread_mutex 互斥鎖的類型不同。通過宏定義,可以簡化方法的定義。

NSLock 比 pthread_mutex 略慢的原因在于它需要經(jīng)過方法調(diào)用,同時(shí)由于緩存的存在,多次方法調(diào)用不會對性能產(chǎn)生太大的影響。

NSCondition

NSCondition 的底層是通過條件變量(condition variable) pthread_cond_t 來實(shí)現(xiàn)的。條件變量有點(diǎn)像信號量,提供了線程阻塞與信號機(jī)制,因此可以用來阻塞某個(gè)線程,并等待某個(gè)數(shù)據(jù)就緒,隨后喚醒線程,比如常見的生產(chǎn)者-消費(fèi)者模式。

如何使用條件變量

很多介紹 pthread_cond_t 的文章都會提到,它需要與互斥鎖配合使用:

void consumer () { // 消費(fèi)者       pthread_mutex_lock(&mutex);     while (data == NULL) {         pthread_cond_wait(&condition_variable_signal, &mutex); // 等待數(shù)據(jù)     }     // --- 有新的數(shù)據(jù),以下代碼負(fù)責(zé)處理 ↓↓↓↓↓↓     // temp = data;     // --- 有新的數(shù)據(jù),以上代碼負(fù)責(zé)處理 ↑↑↑↑↑↑     pthread_mutex_unlock(&mutex); } void producer () {       pthread_mutex_lock(&mutex);     // 生產(chǎn)數(shù)據(jù)     pthread_cond_signal(&condition_variable_signal); // 發(fā)出信號給消費(fèi)者,告訴他們有了新的數(shù)據(jù)     pthread_mutex_unlock(&mutex); } 復(fù)制代碼

自然我們會有疑問:“如果不用互斥鎖,只用條件變量會有什么問題呢?”。問題在于,temp = data; 這段代碼不是線程安全的,也許在你把 data 讀出來以前,已經(jīng)有別的線程修改了數(shù)據(jù)。因此我們需要保證消費(fèi)者拿到的數(shù)據(jù)是線程安全的。

wait 方法除了會被 signal 方法喚醒,有時(shí)還會被虛假喚醒,所以需要這里 while 循環(huán)中的判斷來做二次確認(rèn)。

為什么要使用條件變量

介紹條件變量的文章非常多,但大多都對一個(gè)一個(gè)基本問題避而不談:“為什么要用條件變量?它僅僅是控制了線程的執(zhí)行順序,用信號量或者互斥鎖能不能模擬出類似效果?”

網(wǎng)上的相關(guān)資料比較少,我簡單說一下個(gè)人看法。信號量可以一定程度上替代 condition,但是互斥鎖不行。在以上給出的生產(chǎn)者-消費(fèi)者模式的代碼中, pthread_cond_wait 方法的本質(zhì)是鎖的轉(zhuǎn)移,消費(fèi)者放棄鎖,然后生產(chǎn)者獲得鎖,同理,pthread_cond_signal 則是一個(gè)鎖從生產(chǎn)者到消費(fèi)者轉(zhuǎn)移的過程。

如果使用互斥鎖,我們需要把代碼改成這樣:

void consumer () { // 消費(fèi)者       pthread_mutex_lock(&mutex);     while (data == NULL) {         pthread_mutex_unlock(&mutex);         pthread_mutex_lock(&another_lock)  // 相當(dāng)于 wait 另一個(gè)互斥鎖         pthread_mutex_lock(&mutex);     }     pthread_mutex_unlock(&mutex); } 復(fù)制代碼

這樣做存在的問題在于,在等待 anotherlock 之前, 生產(chǎn)者有可能先執(zhí)行代碼, 從而釋放了 anotherlock。也就是說,我們無法保證釋放鎖和等待另一個(gè)鎖這兩個(gè)操作是原子性的,也就無法保證“先等待、后釋放 another_lock” 這個(gè)順序。

用信號量則不存在這個(gè)問題,因?yàn)樾盘柫康牡却蛦拘巡⒉恍枰獫M足先后順序,信號量只表示有多少個(gè)資源可用,因此不存在上述問題。然而與 pthread_cond_wait 保證的原子性鎖轉(zhuǎn)移相比,使用信號量似乎存在一定風(fēng)險(xiǎn)(暫時(shí)沒有查到非原子性操作有何不妥)。

不過,使用 condition 有一個(gè)好處,我們可以調(diào)用 pthread_cond_broadcast 方法通知所有等待中的消費(fèi)者,這是使用信號量無法實(shí)現(xiàn)的。

NSCondition 的做法

NSCondition 其實(shí)是封裝了一個(gè)互斥鎖和條件變量, 它把前者的 lock 方法和后者的 wait/signal 統(tǒng)一在 NSCondition 對象中,暴露給使用者:

- (void) signal {   pthread_cond_signal(&_condition); } // 其實(shí)這個(gè)函數(shù)是通過宏來定義的,展開后就是這樣 - (void) lock {   int err = pthread_mutex_lock(&_mutex); } 復(fù)制代碼

它的加解鎖過程與 NSLock 幾乎一致,理論上來說耗時(shí)也應(yīng)該一樣(實(shí)際測試也是如此)。在圖中顯示它耗時(shí)略長,我猜測有可能是測試者在每次加解鎖的前后還附帶了變量的初始化和銷毀操作。

NSRecursiveLock

上文已經(jīng)說過,遞歸鎖也是通過 pthread_mutex_lock 函數(shù)來實(shí)現(xiàn),在函數(shù)內(nèi)部會判斷鎖的類型,如果顯示是遞歸鎖,就允許遞歸調(diào)用,僅僅將一個(gè)計(jì)數(shù)器加一,鎖的釋放過程也是同理。

NSRecursiveLock 與 NSLock 的區(qū)別在于內(nèi)部封裝的 pthread_mutex_t 對象的類型不同,前者的類型為 PTHREAD_MUTEX_RECURSIVE。

NSConditionLock

NSConditionLock 借助 NSCondition 來實(shí)現(xiàn),它的本質(zhì)就是一個(gè)生產(chǎn)者-消費(fèi)者模型?!皸l件被滿足”可以理解為生產(chǎn)者提供了新的內(nèi)容。NSConditionLock 的內(nèi)部持有一個(gè) NSCondition 對象,以及 _condition_value 屬性,在初始化時(shí)就會對這個(gè)屬性進(jìn)行賦值:

// 簡化版代碼 - (id) initWithCondition: (NSInteger)value {     if (nil != (self = [super init])) {         _condition = [NSCondition new]         _condition_value = value;     }     return self; } 復(fù)制代碼

它的 lockWhenCondition 方法其實(shí)就是消費(fèi)者方法:

- (void) lockWhenCondition: (NSInteger)value {     [_condition lock];     while (value != _condition_value) {         [_condition wait];     } } 復(fù)制代碼

對應(yīng)的 unlockWhenCondition 方法則是生產(chǎn)者,使用了 broadcast 方法通知了所有的消費(fèi)者:

- (void) unlockWithCondition: (NSInteger)value {     _condition_value = value;     [_condition broadcast];     [_condition unlock]; } 復(fù)制代碼

@synchronized

這其實(shí)是一個(gè) OC 層面的鎖, 主要是通過犧牲性能換來語法上的簡潔與可讀。

我們知道 @synchronized 后面需要緊跟一個(gè) OC 對象,它實(shí)際上是把這個(gè)對象當(dāng)做鎖來使用。這是通過一個(gè)哈希表來實(shí)現(xiàn)的,OC 在底層使用了一個(gè)互斥鎖的數(shù)組(你可以理解為鎖池),通過對對象去哈希值來得到對應(yīng)的互斥鎖。

“怎么理解iOS開發(fā)中的鎖”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

網(wǎng)站名稱:怎么理解iOS開發(fā)中的鎖
標(biāo)題鏈接:http://bm7419.com/article36/gipgsg.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供Google、云服務(wù)器標(biāo)簽優(yōu)化、企業(yè)建站全網(wǎng)營銷推廣、搜索引擎優(yōu)化

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

成都定制網(wǎng)站建設(shè)