1. 為什么多線程需要鎖?
首先在多線程處理的時(shí)候我們經(jīng)常會(huì)需要保證同步,這是為啥呢,看一下下面這個(gè)例子:
NSInteger count;
count = 50;
- (void)test {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (NSInteger i = 0; i < 10; i++) {
[self lockSection];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (NSInteger i = 0; i < 10; i++) {
[self lockSection];
}
});
}
- (void)lockSection {
NSLog(@"before count: %ld", (long)count);
count = count - 1;
NSLog(@"after count: %ld", (long)count);
}
這種時(shí)候我們期待的輸出大概就是按順序,50、49、48……這種,但是實(shí)際上嘞:
2019-11-04 14:57:22.584024+0800 [14572:162956] before count: 50
2019-11-04 14:57:22.584022+0800 [14572:162947] before count: 50
2019-11-04 14:57:22.584123+0800 [14572:162956] after count: 49
2019-11-04 14:57:22.584186+0800 [14572:162956] before count: 49
2019-11-04 14:57:22.584206+0800 [14572:162947] after count: 48
2019-11-04 14:57:22.584254+0800 [14572:162956] after count: 47
2019-11-04 14:57:22.584270+0800 [14572:162947] before count: 47
2019-11-04 14:57:22.584323+0800 [14572:162956] before count: 47
2019-11-04 14:57:22.584535+0800 [14572:162947] after count: 46
2019-11-04 14:57:22.584621+0800 [14572:162956] after count: 45
2019-11-04 14:57:22.584934+0800 [14572:162956] before count: 45
2019-11-04 14:57:22.585000+0800 [14572:162947] before count: 45
2019-11-04 14:57:22.585289+0800 [14572:162956] after count: 44
2019-11-04 14:57:22.585544+0800 [14572:162956] before count: 43
2019-11-04 14:57:22.585499+0800 [14572:162947] after count: 43
2019-11-04 14:57:22.585783+0800 [14572:162956] after count: 42
2019-11-04 14:57:22.586086+0800 [14572:162956] before count: 42
2019-11-04 14:57:22.586100+0800 [14572:162947] before count: 42
2019-11-04 14:57:22.586301+0800 [14572:162956] after count: 41
2019-11-04 14:57:22.586618+0800 [14572:162947] after count: 40
2019-11-04 14:57:22.586651+0800 [14572:162956] before count: 40
2019-11-04 14:57:22.587034+0800 [14572:162956] after count: 39
2019-11-04 14:57:22.586989+0800 [14572:162947] before count: 40
……
感受一下這個(gè)bug,為啥已經(jīng)count=50的時(shí)候進(jìn)入了但是下次進(jìn)入又是50呢,其實(shí)這個(gè)就是線程的問題了,系統(tǒng)在運(yùn)行的時(shí)候每個(gè)線程都是干一會(huì)兒活就會(huì)讓出時(shí)間片,讓其他線程再干一會(huì)兒,交替執(zhí)行。
于是可能線程1剛進(jìn)入count=50的時(shí)候,下一步count=count-1還沒執(zhí)行就讓出了時(shí)間片,于是進(jìn)程2就進(jìn)入了,這個(gè)時(shí)候由于count仍舊是50,所以打印的就會(huì)重復(fù)啦。
所以如果我們想保證不要出現(xiàn)多線程的沖突問題,就需要線程同步了。其實(shí)xcode提供檢測多線程操作同一數(shù)據(jù)的工具非常方便,通過edit schema就可以啦:

勾選這個(gè)選項(xiàng),再次運(yùn)行程序,會(huì)發(fā)現(xiàn)它在出現(xiàn)多線程訪問同一數(shù)據(jù)并且對(duì)它進(jìn)行操作的時(shí)候斷點(diǎn):

2. 如何進(jìn)行多線程同步
主要還是靠加鎖。。iOS有很多鎖,包括:NSLock、semaphore、OSSpinLock神馬的。根據(jù)參考文章里面的test,他們加解鎖的時(shí)間大概如下:

2.1 @synchronized
先來看加鎖最慢的一個(gè),也是在Android中蠻常用到的一個(gè)~
如果改寫為:
- (void)lockSection {
@synchronized (self) {
NSLog(@"before count: %ld", (long)count);
count = count - 1;
NSLog(@"after count: %ld", (long)count);
}
}
輸出就變?yōu)榱耍?/p>
2019-11-18 16:29:58.503712+0800 Example1[45488:373901] before count: 50
2019-11-18 16:29:58.503825+0800 Example1[45488:373901] after count: 49
2019-11-18 16:29:58.503907+0800 Example1[45488:373901] before count: 49
2019-11-18 16:29:58.503986+0800 Example1[45488:373901] after count: 48
2019-11-18 16:29:58.504053+0800 Example1[45488:373901] before count: 48
2019-11-18 16:29:58.504115+0800 Example1[45488:373901] after count: 47
2019-11-18 16:29:58.504244+0800 Example1[45488:373901] before count: 47
2019-11-18 16:29:58.504304+0800 Example1[45488:373901] after count: 46
2019-11-18 16:29:58.504370+0800 Example1[45488:373901] before count: 46
2019-11-18 16:29:58.504534+0800 Example1[45488:373901] after count: 45
2019-11-18 16:29:58.504734+0800 Example1[45488:373901] before count: 45
2019-11-18 16:29:58.504951+0800 Example1[45488:373901] after count: 44
據(jù)說 @synchronized block 會(huì)變成 objc_sync_enter 和 objc_sync_exit 的成對(duì)兒調(diào)用。
@synchronized(obj) {
//do work
}
轉(zhuǎn)化成這樣的東東:
@try {
objc_sync_enter(obj);
//do work
} @finally {
objc_sync_exit(obj);
}
查看objc-sync.h文件可以發(fā)現(xiàn):
#ifndef __OBJC_SNYC_H_
#define __OBJC_SNYC_H_
#include <objc/objc.h>
/**
* Begin synchronizing on 'obj'.
* Allocates recursive pthread_mutex associated with 'obj' if needed.
*
* @param obj The object to begin synchronizing on.
*
* @return OBJC_SYNC_SUCCESS once lock is acquired.
*/
OBJC_EXPORT int
objc_sync_enter(id _Nonnull obj)
OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);
/**
* End synchronizing on 'obj'.
*
* @param obj The object to end synchronizing on.
*
* @return OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
*/
OBJC_EXPORT int
objc_sync_exit(id _Nonnull obj)
OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);
enum {
OBJC_SYNC_SUCCESS = 0,
OBJC_SYNC_NOT_OWNING_THREAD_ERROR = -1
};
#endif // __OBJC_SYNC_H_
也就是說enter的時(shí)候其實(shí)是用pthread_mutex鎖來實(shí)現(xiàn)了lock,根據(jù)源碼https://opensource.apple.com/source/objc4/objc4-646/runtime/objc-sync.mm看一下到底做了些什么:
// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");
result = recursive_mutex_lock(&data->mutex);
require_noerr_string(result, done, "mutex_lock failed");
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
done:
return result;
}
// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR, "id2data failed");
result = recursive_mutex_unlock(&data->mutex);
require_noerr_string(result, done, "mutex_unlock failed");
} else {
// @synchronized(nil) does nothing
}
done:
if ( result == RECURSIVE_MUTEX_NOT_LOCKED )
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
return result;
}
也就是在enter和exit的時(shí)候都用了SyncData,然后通過recursive_mutex_lock和unlock操作data中的mutex。
那么SyncData是什么呢?
typedef struct SyncData {
id object;
recursive_mutex_t mutex;
struct SyncData* nextData;
int threadCount;
} SyncData;
typedef struct SyncList {
SyncData *data;
spinlock_t lock;
} SyncList;
// Use multiple parallel lists to decrease contention among unrelated objects.
#define COUNT 16
#define HASH(obj) ((((uintptr_t)(obj)) >> 5) & (COUNT - 1))
#define LOCK_FOR_OBJ(obj) sDataLists[HASH(obj)].lock
#define LIST_FOR_OBJ(obj) sDataLists[HASH(obj)].data
static SyncList sDataLists[COUNT];
也就是說,有一個(gè)容量為16的SyncList數(shù)組,通過把obj的內(nèi)存地址轉(zhuǎn)為無符號(hào)整型右移5位然后和15的二進(jìn)制表示做與運(yùn)算,可以得到數(shù)組的下標(biāo)。
拿到對(duì)應(yīng)的SyncList以后,就可以拿到SyncData,然后通過data的mutex來上鎖;spinlock_t是用來Spinlock prevents multiple threads from creating multiple locks for the same new object.。
SyncData結(jié)構(gòu)體包含:
- 一個(gè) object(嗯就是我們給 @synchronized 傳入的那個(gè)對(duì)象)。
- 一個(gè)有關(guān)聯(lián)的 recursive_mutex_t,它就是那個(gè)跟 object 關(guān)聯(lián)在一起的鎖。
- 一個(gè)指向另一個(gè) SyncData 對(duì)象的指針,叫做 nextData,所以你可以把每個(gè) SyncData 結(jié)構(gòu)體看做是鏈表中的一個(gè)元素。
- 一個(gè) threadCount,這個(gè) SyncData 對(duì)象中的鎖會(huì)被一些線程使用或等待,threadCount 就是此時(shí)這些線程的數(shù)量。它很有用處,因?yàn)?SyncData 結(jié)構(gòu)體會(huì)被緩存,threadCount==0 就暗示了這個(gè) SyncData 實(shí)例可以被復(fù)用。
你可以把 SyncData 當(dāng)做是鏈表中的節(jié)點(diǎn)。每個(gè) SyncList 結(jié)構(gòu)體都有個(gè)指向 SyncData 節(jié)點(diǎn)鏈表頭部的指針,也有一個(gè)用于防止多個(gè)線程對(duì)此列表做并發(fā)修改的鎖spinlock_t。
這里特別看下鏈表是啥:
// malloc a new SyncData and add to list.
// XXX calling malloc with a global lock held is bad practice,
// might be worth releasing the lock, mallocing, and searching again.
// But since we never free these guys we won't be stuck in malloc very often.
result = (SyncData*)calloc(sizeof(SyncData), 1);
result->object = object;
result->threadCount = 1;
recursive_mutex_init(&result->mutex);
result->nextData = *listp;
*listp = result;
在找obj對(duì)應(yīng)的SyncData的時(shí)候,會(huì)先通過obj內(nèi)存地址轉(zhuǎn)換成下標(biāo)以后找的sync list,然后從list的sync data開始找,不斷地next來找鏈表上是不是已經(jīng)存在了obj對(duì)應(yīng)的sync data。如果沒有就創(chuàng)建一個(gè)SyncData,然后讓這個(gè)新建的SyncData的next指向現(xiàn)在list的頭data,并且將新建的data設(shè)為新的list頭。
https://blog.csdn.net/TuGeLe/article/details/88399115里面的流程圖總結(jié)的很好~
通過clang轉(zhuǎn)為源碼據(jù)說是醬紫的:
static void _I_CustomObject_testSynchronized(CustomObject * self, SEL _cmd) {
{
id _rethrow = 0;
id _sync_obj = (id)self;
objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
} _sync_exit(_sync_obj);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_p3_pyrv2p4j0gn_yqv6994w1ryr0000gn_T_CustomObject_77509d_mi_0);
} catch (id e) {
_rethrow = e;
}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
} _fin_force_rethow(_rethrow);}
}
需要注意的是一開始就id _sync_obj = (id)self;保存了self也就是synchronized的object,即使之后這個(gè)object被置為了nil,由于_sync_obj還拿著引用計(jì)數(shù),就不會(huì)被清掉內(nèi)存。
但這個(gè)也說明了一個(gè)事情,就是被用于synchronized的只能是object。
Q: 那么為什么需要_sync_obj呢?
從 objc_sync_enter和 objc_sync_exit實(shí)現(xiàn)可知,當(dāng)加鎖條件為nil時(shí),臨界區(qū)代碼正常執(zhí)行,但無法加鎖解鎖,不能保證臨界區(qū)代碼在線程中的安全。
首先objc_sync_enter和objc_sync_exit如果傳nil的話,其實(shí)代碼里面什么都沒做,只有obj不為空才會(huì)lock和unlock。
如果objc_sync_enter的時(shí)候obj不為空,lock了mutex鎖,然后exit的時(shí)候傳入了nil,那么這個(gè)mutex鎖不會(huì)被unlock,就會(huì)導(dǎo)致死鎖。
所以其實(shí)_sync_obj的作用就是keep住傳入的object,確保它的引用計(jì)數(shù)不會(huì)為0,也就不會(huì)被清掉,所以lock和unlock是成對(duì)的。
Q: 既然@synchronized對(duì)加鎖條件進(jìn)行了強(qiáng)引用保護(hù),那么是否可以在臨界區(qū)代碼中對(duì)加鎖條件進(jìn)行更改?
不建議在臨界區(qū)代碼中對(duì)加鎖條件進(jìn)行更改的操作。
原因在于若在臨界區(qū)代碼中對(duì)加鎖條件進(jìn)行更改,那么此時(shí)如果再次對(duì)該加鎖條件進(jìn)行加鎖,此時(shí)獲取的 SyncData為不同對(duì)象對(duì)應(yīng)的值,雖說也能成功加鎖,但是無法保證與第一次加鎖線程互斥,可能造成業(yè)務(wù)邏輯的錯(cuò)誤。
Q: 是否可以對(duì)所有需要鎖的操作都使用同一個(gè)加鎖條件?
不建議對(duì)所有需要鎖的操作使用同一個(gè)加鎖條件。
原因在于當(dāng)某個(gè)操作對(duì)加鎖條件進(jìn)行加鎖后,若其他與該操作無關(guān)的操作再對(duì)加鎖條件進(jìn)行加鎖時(shí),需等到前一個(gè)操作執(zhí)行完畢,這可能造成無關(guān)操作多余無用的等待時(shí)間,造成程序效率低下。
所以建議對(duì)涉及共同資源的操作使用同一個(gè)加鎖條件進(jìn)行加鎖,相互無關(guān)的操作使用不同的加鎖條件加鎖。
2.2 NSLock
NSLock 是 Objective-C 以對(duì)象的形式暴露給開發(fā)者的一種鎖,它的實(shí)現(xiàn)非常簡單,通過宏,定義了 lock 方法:
#define MLOCK
- (void) lock {
int err = pthread_mutex_lock(&_mutex);\
// 錯(cuò)誤處理 ……
}
NSLock 只是在內(nèi)部封裝了一個(gè) pthread_mutex,屬性為 PTHREAD_MUTEX_ERRORCHECK,它會(huì)損失一定性能換來錯(cuò)誤提示。
這里使用宏定義的原因是,OC 內(nèi)部還有其他幾種鎖,他們的 lock 方法都是一模一樣,僅僅是內(nèi)部 pthread_mutex 互斥鎖的類型不同。通過宏定義,可以簡化方法的定義。
NSLock 比 pthread_mutex 略慢的原因在于它需要經(jīng)過方法調(diào)用,同時(shí)由于緩存的存在,多次方法調(diào)用不會(huì)對(duì)性能產(chǎn)生太大的影響。
Q: 插一個(gè)之前面試的時(shí)候一個(gè)帥氣冷漠小哥哥問我的問題,如何用NSLock實(shí)現(xiàn)讓任務(wù)A和B執(zhí)行以后再執(zhí)行C?
A: 后來問了另一個(gè)帥氣不冷漠小哥哥,他提醒我可以用兩把鎖...感覺自己智商拙計(jì)了~
2.3 NSRecursiveLock
上文已經(jīng)說過,遞歸鎖也是通過 pthread_mutex_lock 函數(shù)來實(shí)現(xiàn),在函數(shù)內(nèi)部會(huì)判斷鎖的類型,如果顯示是遞歸鎖,就允許遞歸調(diào)用,僅僅將一個(gè)計(jì)數(shù)器加一,鎖的釋放過程也是同理。
NSRecursiveLock 與 NSLock 的區(qū)別在于內(nèi)部封裝的 pthread_mutex_t 對(duì)象的類型不同,前者的類型為 PTHREAD_MUTEX_RECURSIVE。
2.4 dispatch_semaphore信號(hào)量
dispatch_semaphore_t 最終會(huì)調(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;
}
首先會(huì)把信號(hào)量的值減一,并判斷是否大于零。如果大于零,說明不用等待,所以立刻返回。具體的等待操作在 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),主動(dòng)讓出時(shí)間片,這個(gè)函數(shù)在互斥鎖的實(shí)現(xiàn)中,也有可能被用到。
主動(dòng)讓出時(shí)間片并不總是代表效率高。讓出時(shí)間片會(huì)導(dǎo)致操作系統(tǒng)切換到另一個(gè)線程,這種上下文切換通常需要 10 微秒左右,而且至少需要兩次切換。如果等待時(shí)間很短,比如只有幾個(gè)微秒,忙等就比線程睡眠更高效。
可以看到,自旋鎖和信號(hào)量的實(shí)現(xiàn)都非常簡單,這也是兩者的加解鎖耗時(shí)分別排在第一和第二的原因。再次強(qiáng)調(diào),加解鎖耗時(shí)不能準(zhǔn)確反應(yīng)出鎖的效率(比如時(shí)間片切換就無法發(fā)生),它只能從一定程度上衡量鎖的實(shí)現(xiàn)復(fù)雜程度。
2.5 OSSpinLock自旋鎖
#import <libkern/OSAtomic.h>
OSSpinLock *lock;
lock = OS_SPINLOCK_INIT;
- (void)lockSection {
OSSpinLockLock(&lock);
NSLog(@"before count: %ld", (long)count);
count = count - 1;
NSLog(@"after count: %ld", (long)count);
OSSpinLockUnlock(&lock);
}
自旋鎖的目的是為了確保臨界區(qū)只有一個(gè)線程可以訪問,它的使用可以用下面這段偽代碼來描述:
bool lock = false; // 一開始沒有鎖上,任何線程都可以申請(qǐng)鎖
do {
while(lock); // 如果 lock 為 true 就一直死循環(huán),相當(dāng)于申請(qǐng)鎖
lock = true; // 掛上鎖,這樣別的線程就無法獲得鎖
Critical section // 臨界區(qū)
lock = false; // 相當(dāng)于釋放鎖,這樣別的線程可以進(jìn)入臨界區(qū)
Reminder section // 不需要鎖保護(hù)的代碼
}
這段代碼存在一個(gè)問題: 如果一開始有多個(gè)線程同時(shí)執(zhí)行 while 循環(huán),他們都不會(huì)在這里卡住,而是繼續(xù)執(zhí)行,這樣就無法保證鎖的可靠性了。解決思路也很簡單,只要確保申請(qǐng)鎖的過程是原子操作即可。
狹義上的原子操作表示一條不可打斷的操作,也就是說線程在執(zhí)行操作過程中,不會(huì)被操作系統(tǒng)掛起,而是一定會(huì)執(zhí)行完。在單處理器環(huán)境下,一條匯編指令顯然是原子操作,因?yàn)橹袛嘁惨ㄟ^指令來實(shí)現(xiàn)。
然而在多處理器的情況下,能夠被多個(gè)處理器同時(shí)執(zhí)行的操作任然算不上原子操作。因此,真正的原子操作必須由硬件提供支持,比如 x86 平臺(tái)上如果在指令前面加上 “LOCK” 前綴,對(duì)應(yīng)的機(jī)器碼在執(zhí)行時(shí)會(huì)把總線鎖住,使得其他 CPU不能再執(zhí)行相同操作,從而從硬件層面確保了操作的原子性。
※ 自旋鎖的忙等
如果臨界區(qū)的執(zhí)行時(shí)間過長,使用自旋鎖不是個(gè)好主意。之前我們介紹過時(shí)間片輪轉(zhuǎn)算法,線程在多種情況下會(huì)退出自己的時(shí)間片。其中一種是用完了時(shí)間片的時(shí)間,被操作系統(tǒng)強(qiáng)制搶占。除此以外,當(dāng)線程進(jìn)行 I/O 操作,或進(jìn)入睡眠狀態(tài)時(shí),都會(huì)主動(dòng)讓出時(shí)間片。顯然在 while 循環(huán)中,線程處于忙等狀態(tài),白白浪費(fèi) CPU 時(shí)間。
OSSpinLock編譯會(huì)報(bào)警告已經(jīng)廢棄了,大家也已經(jīng)不再用它了,因?yàn)樗谀骋恍﹫鼍跋乱呀?jīng)不安全了,可以參考不再安全的 OSSpinLock。
新版 iOS 中,系統(tǒng)維護(hù)了 5 個(gè)不同的線程優(yōu)先級(jí)/QoS: background,utility,default,user-initiated,user-interactive。高優(yōu)先級(jí)線程始終會(huì)在低優(yōu)先級(jí)線程前執(zhí)行,一個(gè)線程不會(huì)受到比它更低優(yōu)先級(jí)線程的干擾。這種線程調(diào)度算法會(huì)產(chǎn)生潛在的優(yōu)先級(jí)反轉(zhuǎn)問題,從而破壞了 spin lock。
具體來說,如果一個(gè)低優(yōu)先級(jí)的線程獲得鎖并訪問共享資源,這時(shí)一個(gè)高優(yōu)先級(jí)的線程也嘗試獲得這個(gè)鎖,它會(huì)處于 spin lock 的忙等狀態(tài)從而占用大量 CPU。此時(shí)低優(yōu)先級(jí)線程無法與高優(yōu)先級(jí)線程爭奪 CPU 時(shí)間,從而導(dǎo)致任務(wù)遲遲完不成、無法釋放 lock。這并不只是理論上的問題,libobjc 已經(jīng)遇到了很多次這個(gè)問題了,于是蘋果的工程師停用了 OSSpinLock。
2.6 os_unfair_lock
#import <os/lock.h>
os_unfair_lock lock;
lock = OS_UNFAIR_LOCK_INIT;
- (void)lockSection {
os_unfair_lock_lock(&lock);
NSLog(@"before count: %ld", (long)count);
count = count - 1;
NSLog(@"after count: %ld", (long)count);
os_unfair_lock_unlock(&lock);
}
os_unfair_lock 是蘋果官方推薦的替換OSSpinLock的方案,但是它在iOS10.0以上的系統(tǒng)才可以調(diào)用。os_unfair_lock是一種互斥鎖,它不會(huì)向自旋鎖那樣忙等,而是等待線程會(huì)休眠。
2.7 pthread_mutex
pthread 表示 POSIX thread,定義了一組跨平臺(tái)的線程相關(guān)的 API,pthread_mutex 表示互斥鎖。互斥鎖的實(shí)現(xiàn)原理與信號(hào)量非常相似,不是使用忙等,而是阻塞線程并睡眠,需要進(jìn)行上下文切換。
互斥鎖的常見用法如下:
#include <pthread.h>
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ǐng)鎖
// 臨界區(qū)
pthread_mutex_unlock(&mutex); // 釋放鎖
對(duì)于 pthread_mutex 來說,它的用法和之前沒有太大的改變,比較重要的是鎖的類型,可以有 PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE 等等。
PTHREAD_MUTEX_NORMAL 普通鎖
PTHREAD_MUTEX_RECURSIVE 嵌套鎖
允許同一個(gè)線程對(duì)同一個(gè)鎖成功獲得多次,并通過多次unlock解鎖。如果是不同線程請(qǐng)求,則在加鎖線程解鎖時(shí)重新競爭。PTHREAD_MUTEX_ERRORCHECK 檢錯(cuò)鎖
如果同一個(gè)線程請(qǐng)求同一個(gè)鎖,則返回EDEADLK,否則與PTHREAD_MUTEX_NORMAL類型動(dòng)作相同。這樣就保證當(dāng)不允許多次加鎖時(shí)不會(huì)出現(xiàn)最簡單情況下的死鎖。
一般情況下,一個(gè)線程只能申請(qǐng)一次normal鎖,也只能在獲得鎖的情況下才能釋放鎖,多次申請(qǐng)鎖或釋放未獲得的鎖都會(huì)導(dǎo)致崩潰。假設(shè)在已經(jīng)獲得鎖的情況下再次申請(qǐng)鎖,線程會(huì)因?yàn)榈却i的釋放而進(jìn)入睡眠狀態(tài),因此就不可能再釋放鎖,從而導(dǎo)致死鎖。
然而這種情況經(jīng)常會(huì)發(fā)生,比如某個(gè)函數(shù)申請(qǐng)了鎖,在臨界區(qū)內(nèi)又遞歸調(diào)用了自己。辛運(yùn)的是 pthread_mutex 支持遞歸鎖,也就是允許一個(gè)線程遞歸的申請(qǐng)鎖,只要把 attr 的類型改成 PTHREAD_MUTEX_RECURSIVE 即可。
2.8 pthread_rwlock
讀寫鎖與互斥量類似,不過讀寫鎖允許更高的并行性。互斥量要么是鎖住狀態(tài),要么是不加鎖狀態(tài),而且一次只有一個(gè)線程對(duì)其加鎖。
讀寫鎖可以有三種狀態(tài):讀模式下加鎖狀態(tài),寫模式下加鎖狀態(tài),不加鎖狀態(tài)。一次只有一個(gè)線程可以占有寫模式的讀寫鎖,但是多個(gè)線程可用同時(shí)占有讀模式的讀寫鎖。
讀寫鎖也叫做共享-獨(dú)占鎖,當(dāng)讀寫鎖以讀模式鎖住時(shí),它是以共享模式鎖住的,當(dāng)它以寫模式鎖住時(shí),它是以獨(dú)占模式鎖住的。
常用的接口有:
1、pthread_rwlock_init,初始化鎖
2、pthread_rwlock_rdlock,阻斷性的讀鎖定讀寫鎖
3、pthread_rwlock_tryrdlock,非阻斷性的讀鎖定讀寫鎖
4、pthread_rwlock_wrlock,阻斷性的寫鎖定讀寫鎖
5、pthread_rwlock_trywrlock,非阻斷性的寫鎖定讀寫鎖
6、pthread_rwlock_unlock,解鎖
7、pthread_rwlock_destroy,銷毀鎖釋放
使用:
pthread_rwlock_t rwLock;
pthread_rwlock_init(&rwLock, NULL);
pthread_rwlock_wrlock(&rwLock);
// 寫臨界區(qū)
pthread_rwlock_unlock(&rwLock);
這里我感覺如果不用的時(shí)候最好能把鎖銷毀掉,否則可能會(huì)浪費(fèi)內(nèi)存吧。
2.9 NSCondition
The NSCondition class implements a condition variable whose semantics follow those used for POSIX-style conditions. A condition object acts as both a lock and a checkpoint in a given thread. The lock protects your code while it tests the condition and performs the task triggered by the condition. The checkpoint behavior requires that the condition be true before the thread proceeds with its task. While the condition is not true, the thread blocks. It remains blocked until another thread signals the condition object.
NSCondition 的對(duì)象實(shí)際上作為一個(gè)鎖和一個(gè)線程檢查器:鎖主要為了當(dāng)檢測條件時(shí)保護(hù)數(shù)據(jù)源,執(zhí)行條件引發(fā)的任務(wù);線程檢查器主要是根據(jù)條件決定是否繼續(xù)運(yùn)行線程,即線程是否被阻塞。
使用:
NSConditon *condition =[ [NSCondition alloc]]init;
[condition lock];//一般用于多線程同時(shí)訪問、修改同一個(gè)數(shù)據(jù)源,保證在同一時(shí)間內(nèi)數(shù)據(jù)源只被訪問、修改一次,其他線程的命令需要在lock 外等待,只到unlock ,才可訪問
[condition unlock];//與lock 同時(shí)使用
[condition wait];//讓當(dāng)前線程處于等待狀態(tài)
[condition signal];//CPU發(fā)信號(hào)告訴線程不用在等待,可以繼續(xù)執(zhí)行
- wait:阻塞住當(dāng)前線程,線程會(huì)停在-wait方法中不會(huì)返回,直到被其他線程的-signal方法喚醒。
- waitUntilDate:與-wait方法類似,不過這里多了一個(gè)時(shí)間參數(shù)表示最長阻塞到此時(shí)間為止
- signal:調(diào)用此方法可以喚醒
一個(gè)被-wait方法阻塞著的線程 - broadcast:與-signal類似,不過這個(gè)方法可以喚醒
所有被-wait方法阻塞著的線程
NSCondition 的底層是通過條件變量(condition variable) pthread_cond_t 來實(shí)現(xiàn)的。條件變量有點(diǎn)像信號(hào)量,提供了線程阻塞與信號(hào)機(jī)制,因此可以用來阻塞某個(gè)線程,并等待某個(gè)數(shù)據(jù)就緒,隨后喚醒線程,比如常見的生產(chǎn)者-消費(fèi)者模式。
舉個(gè)例子,生產(chǎn)者and消費(fèi)者:
生產(chǎn)者:
-(void)produce{
self.shouldProduce = YES;
while (self.shouldProduce) {
[self.condition lock];
if (self.collector.count > 0 ) {
[self.condition wait];
}
[self.collector addObject:@"iPhone"];
NSLog(@"生產(chǎn):iPhone");
[self.condition signal];
[self.condition unlock];
}
}
消費(fèi)者:
-(void)consumer{
self.shouldConsumer = YES;
while (self.shouldConsumer) {
[self.condition lock];
if (self.collector.count == 0 ) {
[self.condition wait];
}
NSString *item = [self.collector objectAtIndex:0];
NSLog(@"買入:%@",item);
[self.collector removeObjectAtIndex:0];
[self.condition signal];
[self.condition unlock];
}
}
wait做了什么?如何被喚醒?
當(dāng)線程在wait一個(gè)condition時(shí),condition對(duì)象會(huì)解鎖它的lock,并阻塞線程。當(dāng)condition被signaled,系統(tǒng)會(huì)喚醒線程。condition在wait()或者wait(until:)方法返回之前,會(huì)再次嘗試獲取到它的lock(如果獲取lock不成功會(huì)繼續(xù)保持阻塞狀態(tài))。因此,從線程的角度來看,就好像它總是held the lock線程從-wait返回繼續(xù)執(zhí)行下面代碼的前提是:此線程被其他signal方法喚醒 & 當(dāng)前的condition不再被lock
一般在使用-wait時(shí)會(huì)使用一個(gè)謂詞的修飾來做條件判斷,這是因?yàn)椋?/p>
根據(jù)蘋果官方文檔,-signal方法本身就不完全保證是準(zhǔn)確的,會(huì)存在其他線程沒有調(diào)用-signal方法,但是被wait的線程依然被喚醒的情況。
就算被wait的線程的喚醒時(shí)機(jī)沒有問題,但是在被wait的線程被喚醒到執(zhí)行后面代碼期間,程序狀態(tài)可能會(huì)發(fā)生變化,這也是一個(gè)風(fēng)險(xiǎn)項(xiàng)。所以在要執(zhí)行wait后面代碼時(shí),都要重新判斷當(dāng)前程序狀態(tài)是不是自己期望的。
如果有多個(gè)線程處在wait狀態(tài),那么它們被喚醒的順序?yàn)橄热胂瘸?,即先進(jìn)入wait狀態(tài)的線程先被喚醒。
一個(gè)錯(cuò)誤:if or while來判斷condition?
上面的例子中我們用了:
消費(fèi)者:
if (self.collector.count == 0 ) {
[self.condition wait];
}
并且在之后做了[self.collector removeObjectAtIndex:0];這個(gè)事兒。
但是呢注意如果想要從wait繼續(xù)向下走,需要兩個(gè)條件:一個(gè)是被signal喚醒,另一個(gè)是還需要獲取lock。如果另外一個(gè)生產(chǎn)者在生產(chǎn)一個(gè)商品以后發(fā)了signal,然后又清空了產(chǎn)品,最后釋放了鎖。
那么,消費(fèi)者在wait返回時(shí),其實(shí)產(chǎn)品池還是空的,如果此時(shí)用if來判斷self.collector.count == 0,那么其實(shí)走到下面的remove就會(huì)crash,但如果時(shí)用while,那么wait返回以后其實(shí)會(huì)再次判斷count,如果產(chǎn)品池是空的,會(huì)再次wait不會(huì)直接跳出到后面的remove。
故而,在NSCondition的狀況中,應(yīng)該多用while來判斷,而非if來判斷需要wait的狀況。
2.10 NSConditionLock
NSCondition與NSConditionLock非常的像,他們都需要3個(gè)元素:互斥鎖,條件變量,條件探測變量。
不同點(diǎn)是NSCondition需要一個(gè)外部共享變量,來探測條件是否滿足;而NSConditionLock不需要,條件鎖自帶一個(gè)探測條件,是否滿足。
使用:
@interface NSConditionLock : NSObject <NSLocking> {
@private
void *_priv;
}
//初始化一個(gè)NSConditionLock對(duì)象
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition; //鎖的條件
//滿足條件時(shí)加鎖
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
//如果接收對(duì)象的condition與給定的condition相等,則嘗試獲取鎖,不阻塞線程
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
//解鎖后,重置鎖的條件
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
//在指定時(shí)間前嘗試獲取鎖,若成功則返回YES 否則返回NO
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
NSConditionLock與NSCondition大體相同,但是NSConditionLock可以設(shè)置鎖條件condition,而NSCondition確只是無腦的通知信號(hào)。
如果像下面這么用,讓condition值永遠(yuǎn)是一樣的話,其實(shí)就是NSLock:
lock = [[NSConditionLock alloc] initWithCondition:0];
[lock lockWhenCondition:0];
// 臨界區(qū)
[lock unlockWithCondition:0];
如果維持其他的不變,將上鎖改成[lock lockWhenCondition:1];那么其實(shí)lock之后的臨界區(qū)就怎么也進(jìn)不去了,因?yàn)閏ondition初始化為0后從來也沒被修改為1,所以始終wait無法被喚醒。
參考:
https://juejin.im/post/57f6e9f85bbb50005b126e5f#heading-1
http://m.itdecent.cn/p/b1edc6b0937a
http://yulingtianxia.com/blog/2015/11/01/More-than-you-want-to-know-about-synchronized/
https://blog.csdn.net/TuGeLe/article/details/88399115
https://blog.csdn.net/chenyong05314/article/details/54598948
http://m.itdecent.cn/p/5d20c15ae690
http://m.itdecent.cn/p/25b00ce874c6