1诬留、鎖的歸類
鎖的分類只有兩大類自旋鎖和和互斥鎖。這兩大類下又分成很多不同的小類贫母。了解鎖之前建議先了解一下線程及線程安全文兑。
自旋鎖:線程反復檢查鎖變量是否可用。由于線程在這一過程中保持執(zhí)行腺劣,因此是一種忙等待绿贞。一旦獲取自旋鎖,線程會一直保持該鎖橘原,直至顯式釋放自旋鎖籍铁。自旋鎖避免了線程上下文切換的調(diào)度開銷涡上,因此對于線程只會阻塞很短的時間是很高效的,但是對于比較長時間的阻塞也是比較消耗CPU的拒名。iOS常見的自旋鎖:
- OSSpinLock
互斥鎖:是一種用于多線程編程中吩愧,防止兩條線程同時對一公共資源(比如全局變量)進行讀寫的機制。它是通過將代碼切成一個一個的臨界區(qū)而達成的增显。iOS中常見的互斥鎖有:
- NSLock
- pthread_mutex
- @synchronized
條件鎖:就是條件變量雁佳,當進程的某些資源要求不滿足時就進入休眠,也就是鎖住了甸怕。當資源被分配到了甘穿,條件鎖打開腮恩,進程繼續(xù)運行梢杭。iOS中常見的條件鎖:
- NSCondition
- NSConditionLock
遞歸鎖: 就是一個線程可以加鎖N次而不會引發(fā)死鎖。iOS常見的遞歸鎖:
- NSRecursiveLock
- pthread_mutex(recursive)
信號量(semaphore):是一種更高級別的同步機制秸滴,互斥鎖可以說是semaphore在取值0/1時的特例武契。信號量可以有更多的取值空間,用來實現(xiàn)更加復雜的同步荡含,而不單單是線程間互斥咒唆。 iOS的信號量:
- dispatch_semaphore
讀寫鎖: 讀寫鎖實際上是一種特殊的自旋鎖,它把對共享資源的訪問劃分為讀者和寫者释液,讀者只能對共享資源進行訪問全释,寫者則可以對共享資源進行寫操作。這種鎖相對于自旋鎖而言误债,能提高并發(fā)性浸船,因為在多處理器系統(tǒng)中,它允許同時有多個讀者來訪問共享資源寝蹈,最大可能的讀者數(shù)為實際的邏輯CPU數(shù)李命。寫者是排他性的,一個讀寫鎖同時只能有一個寫者或者多個讀者箫老,但不能同時既有讀者又有寫者封字。在讀寫鎖保持期間也是搶占失效的。
如果讀寫鎖當前沒有讀者耍鬓,也沒有寫者阔籽,那么寫者可以立刻獲得資源,否則必須自選在哪里牲蜀,直到?jīng)]有任何寫者或者讀者笆制。如果讀寫鎖當前沒有寫者,那么讀者可以立即獲得該讀寫鎖各薇,否則讀者必須自旋在那里项贺,直到寫者釋放該讀寫鎖君躺。
一次只有一個線程可以占有寫模式的讀寫鎖,但是可以有多個線程同時占有讀模式的讀寫鎖开缎。正是因為這個原因棕叫,當讀寫鎖是寫加鎖狀態(tài)時,在這個鎖被解鎖之前奕删,所有試圖對這個鎖加鎖的線程都會阻塞俺泣;當讀寫鎖在讀加鎖狀態(tài)時,所有試圖以讀模式對它進行加鎖的線程都可以得到訪問權(quán)限完残,但是如果線程希望以寫模式對此鎖進行加鎖伏钠,它必須等到所有線程釋放鎖。通常谨设,當讀寫鎖處于讀模式時熟掂,如果另外線程試圖以寫模式加鎖,讀寫鎖通常會阻塞隨后的讀模式鎖請求扎拣,這樣可以避免讀模式長期占用而導致等待的寫模式線程長期阻塞赴肚。
讀寫鎖適合用于對數(shù)據(jù)結(jié)構(gòu)的讀次數(shù)比寫次數(shù)多得多的情況。因為二蓝,讀模式鎖定時可以共享誉券,以寫模式鎖住時意味著獨占,所以讀寫鎖又叫共享-獨占鎖刊愚。iOS中的讀寫鎖:
- pthread_rwlock_rlock
- pthread_rwlock_wlock
2踊跟、iOS中常見鎖的原理及使用
2.1、@synchronized
使用@synchronized同步代碼示例:
NSObject *obj = [[NSObject alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
@synchronized(obj){
NSLog(@"線程1 start");
sleep(2);
NSLog(@"線程1 end");
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
sleep(1);
@synchronized(obj){
NSLog(@"線程2");
}
});
這段代碼的打印結(jié)果是:
21-07-19 15:53:49.053015+0800 iOSLockTestDemo[15604:9853037] 線程1 start
2021-07-19 15:53:51.054994+0800 iOSLockTestDemo[15604:9853037] 線程1 end
2021-07-19 15:53:51.055214+0800 iOSLockTestDemo[15604:9853030] 線程2
@synchronized(obj)指令使用的obj為該鎖的唯一標識鸥诽,只有當標識相同時商玫,才為滿足互斥,如果線程2中的@synchronized(obj)改為@synchronized(self),剛線程2就不會被阻塞衙传,代碼示例如下:
NSObject *obj = [[NSObject alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
@synchronized(obj){
NSLog(@"線程1 start");
sleep(2);
NSLog(@"線程1 end");
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
sleep(1);
@synchronized(self){
NSLog(@"線程3");
}
});
以上代碼打印結(jié)果:
2021-07-19 15:55:20.861569+0800 iOSLockTestDemo[15631:9855072] 線程1 start
2021-07-19 15:55:21.865265+0800 iOSLockTestDemo[15631:9855076] 線程3
2021-07-19 15:55:22.865633+0800 iOSLockTestDemo[15631:9855072] 線程1 end
@synchronized還是個遞歸可重入鎖决帖,如下代碼所示:
NSObject *obj = [[NSObject alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
@synchronized(obj){
NSLog(@"線程開始");
@synchronized (obj) {
NSLog(@"線程4重入1次開始");
@synchronized (obj) {
NSLog(@"線程4重入2次");
}
NSLog(@"線程4重入1次完成");
}
NSLog(@"線程結(jié)束");
}
});
上述代碼打印結(jié)果:
2021-07-19 17:08:29.120607+0800 iOSLockTestDemo[17806:1063480] 線程開始
2021-07-19 17:08:29.120912+0800 iOSLockTestDemo[17806:1063480] 線程4重入1次開始
2021-07-19 17:08:29.121111+0800 iOSLockTestDemo[17806:1063480] 線程4重入2次
2021-07-19 17:08:29.121285+0800 iOSLockTestDemo[17806:1063480] 線程4重入1次完成
2021-07-19 17:08:29.121435+0800 iOSLockTestDemo[17806:1063480] 線程結(jié)束
@synchronized是個遞歸互斥鎖,同一個線程可以重復獲得這個鎖并進入執(zhí)行執(zhí)行塊里面的代碼而不會導致死鎖蓖捶。
@synchronized的優(yōu)點:
- 不需要在代碼中顯式的創(chuàng)建鎖對象地回,便可以實現(xiàn)鎖的機制;
- 遞歸互斥俊鱼,同一個線程可以重復進入而不導致死鎖刻像。
@synchronized的缺點:
- 效率低。@synchronized塊會隱式的添加一個異常處理例程來保護代碼并闲,該處理例程會在異常拋出的時候自動的釋放互斥鎖细睡,這會增加額外的開銷。同時為了實現(xiàn)遞歸互斥可重入帝火,底層使用的是遞歸鎖加上復雜的業(yè)務(wù)邏輯溜徙,也增加了不少的消耗湃缎。
- @synchronized加鎖需要一個token(demo中的obj),在選著token的時候要特別注意不能讓token為nil蠢壹,否則加鎖無效嗓违。
@synchronized底層原理
接下來的問題是@synchronized底層是如何實現(xiàn)遞歸互斥的?是如何實現(xiàn)可重入的呢图贸?其實主要原因是它的底層使用了遞歸鎖蹂季,可重入的原因是底層使用了一個計數(shù)器,用來記錄鎖的次數(shù)疏日。
接下來我們通過源碼來看個究竟偿洁。@synchronized的底層實現(xiàn)有兩個關(guān)鍵函objc_sync_enter和objc_sync_exit:
- objc_sync_enter
獲得鎖成功之后就會調(diào)用這個函數(shù)。它的源碼如下:
// 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);
ASSERT(data);
data->mutex.lock();
} 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();
}
return result;
}
通過源碼可以看出沟优,當obj為nil時會調(diào)用objc_sync_nil()函數(shù)涕滋,這個函數(shù)是直接返回的的,沒有加鎖操作净神,所以這個obj不能為nil何吝。當obj不為空的時候,通過id2data函數(shù)去獲取一個SyncData類型的數(shù)據(jù)結(jié)構(gòu)鹃唯,然后這個結(jié)構(gòu)有個屬性mutex,它是一個遞歸鎖瓣喊,@synchronized加鎖就是通過這個mutex.lock()實現(xiàn)的坡慌。實際上id2data這個函數(shù)實現(xiàn)了很多核心邏輯,其源碼如下:
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);//
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS //tls-本地局部的線程緩存吧
// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
fastCacheOccupied = YES;
if (data->object == object) {
// Found a match in fast cache.
uintptr_t lockCount;
result = data;
lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
if (result->threadCount <= 0 || lockCount <= 0) {
_objc_fatal("id2data fastcache is buggy");
}
switch(why) {
case ACQUIRE: {
lockCount++;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
break;
}
case RELEASE:
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
if (lockCount == 0) {
// remove from fast cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
#endif
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);
if (cache) {
unsigned int i;
for (i = 0; i < cache->used; i++) {
SyncCacheItem *item = &cache->list[i];
if (item->data->object != object) continue;
// Found a match.
result = item->data;
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
switch(why) {
case ACQUIRE:
item->lockCount++;
break;
case RELEASE:
item->lockCount--;
if (item->lockCount == 0) {
// remove from per-thread cache
cache->list[i] = cache->list[--cache->used];
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
// Thread cache didn't find anything.
// Walk in-use list looking for matching object
// Spinlock prevents multiple threads from creating multiple
// locks for the same new object.
// We could keep the nodes in some hash table if we find that there are
// more than 20 or so distinct locks active, but we don't do that now.
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
for (p = *listp; p != NULL; p = p->nextData) {
if ( p->object == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// no SyncData currently associated with object
if ( (why == RELEASE) || (why == CHECK) )
goto done;
// an unused one was found, use it
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
// Allocate a new SyncData and add to list.
// XXX allocating memory with a global lock held is bad practice,
// might be worth releasing the lock, allocating, and searching again.
// But since we never free these guys we won't be stuck in allocation very often.
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;
*listp = result;
done:
lockp->unlock();
if (result) {
// Only new ACQUIRE should get here.
// All RELEASE and CHECK and recursive ACQUIRE are
// handled by the per-thread caches above.
if (why == RELEASE) {
// Probably some thread is incorrectly exiting
// while the object is held by another thread.
return nil;
}
if (why != ACQUIRE) _objc_fatal("id2data is buggy");
if (result->object != object) _objc_fatal("id2data is buggy");
#if SUPPORT_DIRECT_THREAD_KEYS
if (!fastCacheOccupied) {
// Save in fast thread cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
#endif
{
// Save in thread cache
if (!cache) cache = fetch_cache(YES);
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
}
return result;
}
這個源碼里涉及到幾個數(shù)據(jù)結(jié)構(gòu)SyncData藻三、SyncList洪橘、SyncCache、
SyncCacheItem和StripedMap<SyncList> sDataLists棵帽。他們的定義如下:
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;
typedef struct SyncCache {
unsigned int allocated;
unsigned int used;
SyncCacheItem list[0];
} SyncCache;
/*
Fast cache: two fixed pthread keys store a single SyncCacheItem.
This avoids malloc of the SyncCache for threads that only synchronize
a single object at a time.
SYNC_DATA_DIRECT_KEY == SyncCacheItem.data
SYNC_COUNT_DIRECT_KEY == SyncCacheItem.lockCount
*/
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
// Use multiple parallel lists to decrease contention among unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
StripedMap<SyncList> sDataLists 是一個哈希結(jié)構(gòu)熄求,SyncList是SyncData鏈表的表頭,它的結(jié)構(gòu)大致如下:
結(jié)合源碼可以看出逗概,當obj不為空時弟晚,@synchronized的工作流大致如下:
- 1、當一個線程第一次進來的時候逾苫,這時沒有鎖卿城、沒有緩存。這時候會創(chuàng)建一個新的SyncData铅搓,然后關(guān)聯(lián)哈希表sDataLists瑟押,代碼如下:
// Allocate a new SyncData and add to list.
// XXX allocating memory with a global lock held is bad practice,
// might be worth releasing the lock, allocating, and searching again.
// But since we never free these guys we won't be stuck in allocation very often.
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);//創(chuàng)建鎖
result->nextData = *listp;
*listp = result;
創(chuàng)建完成之后寫入緩存然后返回,源碼:
#if SUPPORT_DIRECT_THREAD_KEYS
if (!fastCacheOccupied) {
// Save in fast thread cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
#endif
{
// Save in thread cache
if (!cache) cache = fetch_cache(YES);
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
這時候緩存SyncCache中的對應(yīng)的SyncCacheItem的lockCount=1星掰,SyncData中的threadCount=1多望;
2嫩舟、當同一個線程再次(多次)進來的時候,lockCount++, 緩存怀偷;
3至壤、當不同的線程進來的時候,這時候threadCount++, lockCount++枢纠。
lockCount是保障了單線程可重入而不死鎖像街,而threadCount則保障了不會因為多線程相互等待而導致的死鎖。objc_sync_exit
在代碼塊執(zhí)行結(jié)束的時候會調(diào)用這個函數(shù)進行unlock晋渺。同時lockCount--镰绎,當lockCount <= 0時,threadCount--木西。
通過對threadCount和lockCount的維護來對鎖進行管理畴栖,以實現(xiàn)可重入的目的。
2.2八千、NSLock
NSLock使用很簡單吗讶,它是Cocoa提供給我們最基本的鎖對象,底層是通過pthread_mutex實現(xiàn)的恋捆,這也是我們經(jīng)常所使用的照皆,代碼如下:
NSLock *lock = [[NSLock alloc] init];
for (int i = 0; i < 200; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
self.testArray = [NSMutableArray array];
[lock unlock];
});
}
上面的代碼如果把lock去掉就會崩潰。除lock和unlock方法外沸停,NSLock還提供了tryLock和lockBeforeDate:兩個方法膜毁,前一個方法會嘗試加鎖航邢,如果鎖不可用(已經(jīng)被鎖住)物臂,剛并不會阻塞線程,并返回NO为牍。lockBeforeDate:方法會在所指定Date之前嘗試加鎖能颁,如果在指定時間之前都不能加鎖杂瘸,則返回NO。代碼演示:
NSLock * lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
[lock lockBeforeDate:[NSDate date]];
NSLog(@"線程1 開始");
sleep(2);
NSLog(@"線程1 結(jié)束");
[lock unlock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
sleep(1);
if([lock tryLock]) {//嘗試獲取鎖伙菊,如果獲取不到返回NO败玉,不會阻塞該線程NSLog(@"鎖可用的操作");
[lock unlock];
}else{
NSLog(@"鎖不可用");
}
NSDate*date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];
if([lock lockBeforeDate:date]) {//嘗試在未來的3s內(nèi)獲取鎖,并阻塞該線程占业,如果3s內(nèi)獲取不到恢復線程, 返回NO,不會阻塞該線程
NSLog(@"沒有超時绒怨,能獲得鎖");
[lock unlock];
}else{
NSLog(@"超時,無法獲得鎖");
}
});
上面代碼的執(zhí)行結(jié)果為:
2021-07-20 10:45:13.323313+0800 iOSLockTestDemo[19123:10215889] 線程1 開始
2021-07-20 10:45:14.323755+0800 iOSLockTestDemo[19123:10215890] 鎖不可用
2021-07-20 10:45:15.328511+0800 iOSLockTestDemo[19123:10215889] 線程1 結(jié)束
2021-07-20 10:45:15.328805+0800 iOSLockTestDemo[19123:10215890] 沒有超時谦疾,能獲得鎖
2.3南蹂、NSRecursiveLock
NSRecursiveLock實際上定義的是一個遞歸鎖,這個鎖可以被同一線程多次請求念恍,而不會引起死鎖六剥。這主要是用在循環(huán)或遞歸操作中晚顷。
NSRecursiveLock的使用我們可以與NSLock進行對比來更好的了解,比如如下代碼用NSLock實現(xiàn):
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[lock unlock];
};
testMethod(10);
});
這段代碼是一個典型的死鎖情況疗疟,testMethod在當前線程是遞歸調(diào)用的该默。所以每次進入這個block時,都會去加一次鎖策彤,而從第二次開始栓袖,由于鎖已經(jīng)被使用了且沒有解鎖,所以它需要等待鎖被解除店诗,這樣就導致了死鎖裹刮,線程被阻塞住了。調(diào)試器中會輸出如下信息:
2021-07-20 10:53:23.986601+0800 iOSLockTestDemo[19243:10225964] current value = 10
在這種情況下庞瘸,如果替換成NSRecursiveLock捧弃。它可以允許同一線程多次加鎖,而不會造成死鎖擦囊。遞歸鎖會跟蹤它被lock的次數(shù)违霞。每次成功的lock都必須平衡調(diào)用unlock操作。只有所有達到這種平衡瞬场,鎖最后才能被釋放买鸽,以供其它線程使用。如果我們將NSLock代替為NSRecursiveLock泌类,上面代碼則會正確執(zhí)行癞谒。代碼如下:
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[lock unlock];
};
testMethod(10);
});
執(zhí)行結(jié)果如下:
2021-07-20 10:59:49.184600+0800 iOSLockTestDemo[19296:10230483] current value = 10
2021-07-20 10:59:49.188215+0800 iOSLockTestDemo[19296:10230483] current value = 9
2021-07-20 10:59:49.192839+0800 iOSLockTestDemo[19296:10230483] current value = 8
2021-07-20 10:59:49.196742+0800 iOSLockTestDemo[19296:10230483] current value = 7
2021-07-20 10:59:49.198144+0800 iOSLockTestDemo[19296:10230483] current value = 6
2021-07-20 10:59:49.199597+0800 iOSLockTestDemo[19296:10230483] current value = 5
2021-07-20 10:59:49.200831+0800 iOSLockTestDemo[19296:10230483] current value = 4
2021-07-20 10:59:49.201976+0800 iOSLockTestDemo[19296:10230483] current value = 3
2021-07-20 10:59:49.202657+0800 iOSLockTestDemo[19296:10230483] current value = 2
2021-07-20 10:59:49.206417+0800 iOSLockTestDemo[19296:10230483] current value = 1
像這種遞歸調(diào)用其實也可以用@synchronized來代替的,而且大部分情況下性能差不多刃榨。@synchronized相對來說適用性更廣。
2.4双仍、NSCondition
NSCondition一種最基本的條件鎖枢希。手動控制線程wait和signal。NSCondition類似于生產(chǎn)者-消費者模型朱沃,生產(chǎn)和消費同時進行苞轿,但是只有生產(chǎn)出足夠消費的產(chǎn)品才能開始消費。代碼示例:
NSCondition *condition = [[NSCondition alloc] init];
__block int i = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
while (1) {
[condition lock];
if (i == 0) {
NSLog(@"線程1等待");
[condition wait];
}
i = 0;
NSLog(@"線程1繼續(xù)執(zhí)行");
[condition unlock];
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
while (1) {
[condition lock];
i = 1;
NSLog(@"線程2告訴線程1不用等待");
[condition signal];
[condition unlock];
sleep(1);
}
});
上面代碼執(zhí)行結(jié)果如下:
2021-07-20 12:45:55.868665+0800 iOSLockTestDemo[20075:10303489] 線程1等待
2021-07-20 12:45:55.868964+0800 iOSLockTestDemo[20075:10303485] 線程2告訴線程1不用等待
2021-07-20 12:45:55.869151+0800 iOSLockTestDemo[20075:10303489] 線程1繼續(xù)執(zhí)行
......
[condition lock];一般用于多線程同時訪問逗物、修改同一個數(shù)據(jù)源搬卒,保證在同一時間內(nèi)數(shù)據(jù)源只被訪問、修改一次翎卓,其他線程的命令需要lock 外等待契邀,只到unlock ,才可訪問[condition unlock];與lock 同時使用[condition wait]; 讓當前線程處于等待狀態(tài)[condition signal]; CPU發(fā)信號告訴線程不用在等待失暴,可以繼續(xù)執(zhí)行坯门。
2.5微饥、NSConditionLock
當我們在使用多線程的時候,有時一把只會lock和unlock的鎖未必就能完全滿足我們的使用古戴。因為普通的鎖只能關(guān)心鎖與不鎖欠橘,而不在乎用什么鑰匙才能開鎖,而我們在處理資源共享的時候现恼,多數(shù)情況是只有滿足一定條件的情況下才能打開這把鎖肃续,這時候就需要NSConditionLock(條件鎖),代碼示例:
NSConditionLock *conditionLock = [[NSConditionLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lockWhenCondition:1];
NSLog(@"線程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lockWhenCondition:2];
NSLog(@"線程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"線程 3");
[conditionLock unlockWithCondition:2];
});
上面代碼執(zhí)行結(jié)果如下:
2021-07-20 11:17:32.830479+0800 iOSLockTestDemo[19442:10244097] 線程 3
2021-07-20 11:17:32.848170+0800 iOSLockTestDemo[19442:10244097] 線程 2
2021-07-20 11:17:32.849217+0800 iOSLockTestDemo[19442:10243439] 線程 1
在線程3中的加鎖使用了lock叉袍,所以是不需要條件的始锚,所以順利的就鎖住了,而線程2則需要一把被標識為2的鑰匙, 所以在線程3結(jié)束調(diào)用[conditionLock unlockWithCondition:2]發(fā)送標識2的鑰匙畦韭,才最終打開了線程2中的阻塞疼蛾。同理,線程1也要等到線程2結(jié)束發(fā)送標識為1的鑰匙才能繼續(xù)執(zhí)行艺配。
NSConditionLock實際是NSCondition的進一步封裝察郁,也跟其它的鎖一樣,是需要lock與unlock對應(yīng)的转唉,只是lock,lockWhenCondition:與unlock皮钠,unlockWithCondition:是可以隨意組合的,當然這是與你的需求相關(guān)的赠法。但在unlock的使用了一個整型的條件麦轰,它可以開啟其它線程中正在等待這把鑰匙的臨界地。
2.6砖织、dispatch_semaphore
dispatch_semaphore是GCD用來同步的一種方式款侵,與他相關(guān)的共有三個函數(shù),分別是dispatch_semaphore_create侧纯,dispatch_semaphore_signal新锈,dispatch_semaphore_wait。
(1)dispatch_semaphore_create的聲明為:
dispatch_semaphore_t dispatch_semaphore_create(long value);
傳入的參數(shù)為long眶熬,輸出一個dispatch_semaphore_t類型且值為value的信號量妹笆。
值得注意的是,這里的傳入的參數(shù)value必須大于或等于0娜氏,否則dispatch_semaphore_create會返回NULL拳缠。
(2)dispatch_semaphore_signal的聲明為:
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
這個函數(shù)會使傳入的信號量dsema的值加1;
(3) dispatch_semaphore_wait的聲明為:
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)贸弥;
這個函數(shù)會使傳入的信號量dsema的值減1窟坐;這個函數(shù)的作用是這樣的,如果dsema信號量的值大于0,該函數(shù)所處線程就繼續(xù)執(zhí)行下面的語句狸涌,并且將信號量的值減1切省;如果desema的值為0,那么這個函數(shù)就阻塞當前線程等待timeout(注意timeout的類型為dispatch_time_t帕胆,不能直接傳入整形或float型數(shù))朝捆,如果等待的期間desema的值被dispatch_semaphore_signal函數(shù)加1了,且該函數(shù)(即dispatch_semaphore_wait)所處線程獲得了信號量懒豹,那么就繼續(xù)向下執(zhí)行并將信號量減1芙盘。如果等待期間沒有獲取到信號量或者信號量的值一直為0,那么等到timeout時脸秽,其所處線程自動執(zhí)行其后語句儒老。代碼示例:
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結(jié)束");
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);
});
上面代碼的執(zhí)行結(jié)果為:
2021-07-20 12:50:57.189124+0800 iOSLockTestDemo[20107:10306945] 線程1開始
2021-07-20 12:50:59.193231+0800 iOSLockTestDemo[20107:10306945] 線程1結(jié)束
2021-07-20 12:50:59.193770+0800 iOSLockTestDemo[20107:10306940] 線程2
dispatch_semaphore 是信號量,但當信號總量設(shè)為 1 時也可以當作鎖來记餐。在沒有等待情況出現(xiàn)時驮樊,它的性能比 pthread_mutex 還要高,但一旦有等待情況出現(xiàn)時片酝,性能就會下降許多囚衔。相對于 OSSpinLock 來說,它的優(yōu)勢在于等待時不會消耗 CPU 資源雕沿。
如上的代碼练湿,如果超時時間overTime設(shè)置成>2,可完成同步操作审轮。如果overTime<2的話肥哎,在線程1還沒有執(zhí)行完成的情況下,此時超時了疾渣,將自動執(zhí)行下面的代碼篡诽。
2.7、pthread_mutex
c語言定義下多線程加鎖方式榴捡。
1:pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t attr);初始化鎖變量mutex霞捡。attr為鎖屬性,NULL值為默認屬性薄疚。
2:pthread_mutex_lock(pthread_mutex_tmutex);加鎖
3:pthread_mutex_tylock(pthread_mutex_tmutex);加鎖,但是與2不一樣的是當鎖已經(jīng)在使用的時候赊琳,返回為EBUSY街夭,而不是掛起等待。
4:pthread_mutex_unlock(pthread_mutex_tmutex);釋放鎖
5:pthread_mutex_destroy(pthread_mutex_t***mutex);使用完后釋放
__block pthread_mutex_t theLock;
pthread_mutex_init(&theLock,NULL);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
pthread_mutex_lock(&theLock);
NSLog(@"線程1開始");
sleep(3);
NSLog(@"線程1結(jié)束");
pthread_mutex_unlock(&theLock);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
sleep(1);pthread_mutex_lock(&theLock);
NSLog(@"線程2");
pthread_mutex_unlock(&theLock);
});
}
代碼執(zhí)行操作結(jié)果如下:
2021-07-20 12:54:33.441769+0800 iOSLockTestDemo[20129:10310609] 線程1開始
2021-07-20 12:54:36.443789+0800 iOSLockTestDemo[20129:10310609] 線程1結(jié)束
2021-07-20 12:54:36.444066+0800 iOSLockTestDemo[20129:10309372] 線程2
2.8躏筏、pthread_mutex(recursive)
這是pthread_mutex為了防止在遞歸的情況下出現(xiàn)死鎖而出現(xiàn)的遞歸鎖板丽。作用和NSRecursiveLock遞歸鎖類似。
__block pthread_mutex_t lock;
// pthread_mutex_init(&lock, NULL);//生成普通互斥鎖
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);//生成遞歸互斥鎖
pthread_mutex_init(&lock,&attr);
pthread_mutexattr_destroy(&attr);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
pthread_mutex_lock(&lock);
if (value>0) {
NSLog(@"value = %d", value);
sleep(1);
RecursiveMethod(value-1);
}
pthread_mutex_unlock(&lock);
};
RecursiveMethod(5);
});
如果使用pthread_mutex_init(&lock, NULL);初始化鎖的話,上面的代碼會出現(xiàn)死鎖現(xiàn)象埃碱。如果使用遞歸鎖的形式猖辫,則沒有問題。
2.9砚殿、OSSpinLock& os_unfair_lock
OSSpinLock 自旋鎖啃憎,性能最高的鎖。原理很簡單似炎,就是一直 do while 忙等辛萍。它的缺點是當?shù)却龝r會消耗大量 CPU 資源,所以它不適用于較長時間的任務(wù)羡藐。 不過據(jù)說會導致優(yōu)先級反轉(zhuǎn)導致死鎖的問題贩毕。所謂優(yōu)先級反轉(zhuǎn),簡單說就是如果一條低優(yōu)先級的線程獲取了當前這個鎖仆嗦,還在執(zhí)行任務(wù)辉阶,而這時候有條高優(yōu)先級線程過來獲取當前這個鎖的時候由于鎖已被占用,而高優(yōu)先級線程會一直自旋瘩扼,占用CPU谆甜,導致持有鎖的線程無法獲取CPU資源執(zhí)行后面的代碼釋放鎖,這樣就形成了一個相互等待的死循環(huán)邢隧。OSSpinLock代碼示例:
_spinLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
OSSpinLockLock(&_spinLock);
NSLog(@"線程1開始");
sleep(2);
NSLog(@"線程1完成");
OSSpinLockUnlock(&_spinLock);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
OSSpinLockLock(&_spinLock);
NSLog(@"線程2開始");
sleep(2);
NSLog(@"線程2完成")
OSSpinLockUnlock(&_spinLock);
});
上面的代碼執(zhí)行結(jié)果:
2021-07-20 13:06:51.733227+0800 iOSLockTestDemo[20316:10322092] 線程2開始
2021-07-20 13:06:54.740453+0800 iOSLockTestDemo[20316:10322092] 線程2完成
2021-07-20 13:06:54.783666+0800 iOSLockTestDemo[20316:10322090] 線程1開始
2021-07-20 13:06:57.788149+0800 iOSLockTestDemo[20316:10322090] 線程1完成
由于會導致優(yōu)先級反轉(zhuǎn)的問題店印,所以蘋果已經(jīng)不推薦使用,蘋果底層使用OSSpinLock都已經(jīng)替換成了os_unfair_lock了倒慧。os_unfair_lock的使用代碼示例:
os_unfair_lock unfairLock = OS_UNFAIR_LOCK_INIT;
os_unfair_lock_lock(&unfairLock);
// doSomething
os_unfair_lock_unlock(&unfairLock);
性能對比
上面列舉的是iOS中常用的鎖按摘,他們的實現(xiàn)機制各不相同,性能也各不一樣纫谅。下面引用來源于網(wǎng)絡(luò)的一張圖片炫贤,圖中是對各種鎖進行相同次的多次操作之后得出的結(jié)果,它們多次操作的性能對比如下:
總的來說:
OSSpinLock和dispatch_semaphore的效率遠遠高于其他付秕。@synchronized和NSConditionLock效率較差兰珍。鑒于OSSpinLock的不安全,所以我們在開發(fā)中如果考慮性能的話询吴,建議使用dispatch_semaphore掠河。如果不考慮性能,只是圖個方便的話猛计,那就使用@synchronized唠摹。