補充:
可以看到除了?OSSpinLock?外,dispatch_semaphore?和?pthread_mutex?性能是最高的。蘋果在新系統中已經優化了?pthread_mutex?的性能,所以它看上去和?OSSpinLock?差距并沒有那么大了。
可以看到YYKit組件中YYCache 和 YYImageCoder大量使用dispatch_semaphorepthread_mutex這兩個鎖
OSSpinLock 自旋鎖(雖然已經被證明不安全 優先級翻轉),性能最高的鎖。原理很簡單,就是一直?do?while?忙等。它的缺點是當等待時會消耗大量 CPU 資源,所以它不適用于較長時間的任務。對于內存緩存的存取來說,它非常合適。
dispatch_semaphore?是信號量,但當信號總量設為1?時也可以當作鎖來。在沒有等待情況出現時,它的性能比 pthread_mutex 還要高,但一旦有等待情況出現時,性能就會下降許多。相對于 OSSpinLock 來說,它的優勢在于等待時不會消耗 CPU 資源。對磁盤緩存來說,它比較合適。
不存在等待的情況,例如不涉及到磁盤這種文件讀寫,dispatch_semaphore性能很高,如果涉及到的任務等待時間較長,就需要用pthread_mutex(OSSpinLock不安全就可以先不用了)
寫這個主要是整合下看到的資料,某天突然看到一到騰訊的面試題,里面就有一提就是讓我們談談我們自己所認識的iOS中的鎖,自己平時看到的就在RAC中的OSSPinLock,AF一些大型框架里面的disaptch_semaphore,還有就是NSLock和@synchronize這幾個常見的鎖了,用法是很簡單,但是別人讓你談談你自己的理解,因此就有了下面的資料,主要來自于一下幾篇文章
非常感謝這些大神的資料,對這個知識點有了基本的了解
這里貼兩個面試基礎題目,對后續文章看起來會理解更好一點
QA1:自旋和互斥對比?
自旋鎖和互斥鎖
相同點:都能保證同一時間只有一個線程訪問共享資源。都能保證線程安全。
不同點:
互斥鎖:如果共享數據已經有其他線程加鎖了,線程會進入休眠狀態等待鎖。一旦被訪問的資源被解鎖,則等待資源的線程會被喚醒。
自旋鎖:如果共享數據已經有其他線程加鎖了,線程會以死循環的方式等待鎖,一旦被訪問的資源被解鎖,則等待資源的線程會立即執行。
自旋鎖的效率高于互斥鎖。
使用自旋鎖時要注意:
由于自旋時不釋放CPU,因而持有自旋鎖的線程應該盡快釋放自旋鎖,否則等待該自旋鎖的線程會一直在哪里自旋,這就會浪費CPU時間。
持有自旋鎖的線程在sleep之前應該釋放自旋鎖以便其他咸亨可以獲得該自旋鎖。內核編程中,如果持有自旋鎖的代碼sleep了就可能導致整個系統掛起。
? ? 使用任何鎖都需要消耗系統資源(內存資源和CPU時間),這種資源消耗可以分為兩類:
? ? ? ? 1.建立鎖所需要的資源
? ? ? ? 2.當線程被阻塞時所需要的資源
兩種鎖的加鎖原理:
互斥鎖:線程會從sleep(加鎖)——>running(解鎖),過程中有上下文的切換(主動出讓時間片,線程休眠,等待下一次喚醒),cpu的搶占,信號的發送等開銷。
自旋鎖:線程一直是running(加鎖——>解鎖),死循環(忙等 do-while)檢測鎖的標志位,機制不復雜。
鎖:在計算機科學中,鎖是一種同步機制,用于在存在多線程的環境中實施對資源的訪問限制。你可以理解成它用于排除并發的一種策略!你可以理解為為了防止多線程訪問下資源的搶奪,保持線程同步的方式,以下就是常用的幾個鎖
1.@synchronized?關鍵字加鎖?
2. NSLock?對象鎖?
3. NSCondition ?
4. NSConditionLock?條件鎖?
5. NSRecursiveLock?遞歸鎖?
6. pthread_mutex?互斥鎖(C語言)?
7. dispatch_semaphore?信號量實現加鎖(GCD)?
8. OSSpinLock?自旋鎖?
9.pthread_rwlock
10.POSIX Conditions
11.os_unfair_lock? iOS10之后替代OSSPinLock的鎖,解決了優先級反轉的問題
這幾個比較常見的基本用法可以看這里點擊打開鏈接,這個這里就不介紹了,用法都很簡單
OSSpinLock 不再安全,主要原因發生在低優先級線程拿到鎖時,高優先級線程進入忙等(busy-wait)狀態,消耗大量 CPU 時間,從而導致低優先級線程拿不到 CPU 時間,也就無法完成任務并釋放鎖。這種問題被稱為優先級反轉。
為什么忙等會導致低優先級線程拿不到時間片?這還得從操作系統的線程調度說起。
現代操作系統在管理普通線程時,通常采用時間片輪轉算法(Round Robin,簡稱 RR)。每個線程會被分配一段時間片(quantum),通常在 10-100 毫秒左右。當線程用完屬于自己的時間片以后,就會被操作系統掛起,放入等待隊列中,直到下一次被分配時間片。
忙等這種自旋鎖的實現原理
在 Acquire Lock 這一步,我們申請加鎖,目的是為了保護臨界區(Critical Section) 中的代碼不會被多個線程執行。
上面的偽代碼就是實現自旋鎖的基本原理,初始化一個lock的全局變量,一開始是false,while(lock)的意思是,當lock為true的時候,就進行忙等死循環(do-while申請鎖),由于一開始是false,直接退出循環,然后lock鎖上,執行臨界區代碼,也就是這個時候有其他線程訪問,lock已經被鎖上,while循環會一直忙等,處于申請鎖狀態,上一個鎖執行完任務,就會解鎖,這個時候lock變成了false,之前其他線程忙等狀態下的條件變了,跳出循環,下一個線程執行lock=true,進門執行任務,其他線程繼續等待。這里有個問題,如果一開始有多個線程同時執行 while 循環,他們都不會在這里卡住,而是繼續執行,這樣就無法保證鎖的可靠性了。解決思路也很簡單,只要確保申請鎖的過程是原子操作即可。
用一個原子性操作?test_and_set?來完成,可以理解為線程進來的時候通過鎖的上一個狀態在判斷一次,它用偽代碼可以這樣表示:
該段代碼的意思也很清晰,通過傳入開關地址,先用局部變量存儲一開始開關的狀態,然后內部把開關變成開的狀態,但是返回值是給while的,所以返回值就是開關進來沒操作之前的狀態。
以下就是最終版本的自旋鎖的偽代碼:
如果臨界區的執行時間過長,使用自旋鎖不是個好主意。之前我們介紹過時間片輪轉算法,線程在多種情況下會退出自己的時間片。其中一種是用完了時間片的時間,被操作系統強制搶占。除此以外,當線程進行 I/O 操作,或進入睡眠狀態時,都會主動讓出時間片。顯然在 while 循環中,線程處于忙等狀態,白白浪費 CPU 時間,最終因為超時被操作系統搶占時間片。如果臨界區執行時間較長,比如是文件讀寫,這種忙等是毫無必要的。
2.dispatch_semaphore(信號量? 互斥類型)
dispatch_semaphore是GCD用來同步的一種方式,與他相關的共有三個函數,分別是dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。
(1)dispatch_semaphore_create的聲明為:
dispatch_semaphore_t dispatch_semaphore_create(long value);
傳入的參數為long,輸出一個dispatch_semaphore_t類型且值為value的信號量。
值得注意的是,這里的傳入的參數value必須大于或等于0,否則dispatch_semaphore_create會返回NULL。
(2)dispatch_semaphore_signal的聲明為:
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
這個函數會使傳入的信號量dsema的值加1;
(3) dispatch_semaphore_wait的聲明為:
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 是信號量,但當信號總量設為 1 時也可以當作鎖來。在沒有等待情況出現時,它的性能比 pthread_mutex 還要高,但一旦有等待情況出現時,性能就會下降許多。相對于 OSSpinLock 來說,它的優勢在于等待時不會消耗 CPU 資源。
信號量實現內部代碼:
首先會把信號量的值減一,并判斷是否大于零。如果大于零,說明不用等待,所以立刻返回。具體的等待操作在lll_futex_wait函數中實現,lll是 low level lock 的簡稱。這個函數通過匯編代碼實現,調用到SYS_futex這個系統調用,使線程進入睡眠狀態,主動讓出時間片,這個函數在互斥鎖的實現中,也有可能被用到。
主動讓出時間片(互斥的做法,休眠)并不總是代表效率高。讓出時間片會導致操作系統切換到另一個線程,這種上下文切換通常需要 10 微秒左右,而且至少需要兩次切換。如果等待時間很短,比如只有幾個微秒,忙等就比線程睡眠更高效。
互斥鎖的實現原理與信號量非常相似,不是使用忙等,而是阻塞線程并睡眠,需要進行上下文切換。
對于 pthread_mutex 來說,它的用法和之前沒有太大的改變,比較重要的是鎖的類型,可以有?PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE等等,具體的特性就不做解釋了,網上有很多相關資料。其中如果lock區域代碼,再次進行加鎖,就會變成遞歸,因此我們要把鎖的屬性變成PTHREAD_MUTEX_RECURSIVE來避免死鎖
互斥鎖的實現
互斥鎖在申請鎖時,調用了pthread_mutex_lock方法,它在不同的系統上實現各有不同,有時候它的內部是使用信號量來實現,即使不用信號量,也會調用到lll_futex_wait函數,從而導致線程休眠。
上文說到如果臨界區很短,忙等的效率也許更高,所以在有些版本的實現中,會首先嘗試一定次數(比如 1000 次)的 testandtest,這樣可以在錯誤使用互斥鎖時提高性能。
另外,由于pthread_mutex有多種類型,可以支持遞歸鎖等,因此在申請加鎖時,需要對鎖的類型加以判斷,這也就是為什么它和信號量的實現類似,但效率略低的原因。
4.NSLock,NSCondition,NSRecursiveLock
這些只是上面幾個的上層封裝,可以看上面提供的文章,這里不展開了
這其實是一個 OC 層面的鎖, 主要是通過犧牲性能換來語法上的簡潔與可讀。
我們知道 @synchronized 后面需要緊跟一個 OC 對象,它實際上是把這個對象當做鎖來使用。這是通過一個哈希表來實現的,OC 在底層使用了一個互斥鎖的數組(你可以理解為鎖池),通過對對象去哈希值來得到對應的互斥鎖。
其實這個哈希表的實現和weak屬性一樣,這里是以對象為key,然后互斥鎖的數組為value,weak屬性是以對象為key,指向該對象的weak指針為數組value,當對象dealloc的時候,調用一系列函數,找到該對象的value數組,把數組指針都置為nil,然后再把key對應的記錄全部清空。
原文:https://blog.csdn.net/deft_mkjing/article/details/79513500