iOS 線程鎖

概念

  1. 自旋鎖
    1.1 OSSpinLock
    1.2 os_unfair_lock
    1.3 atomic
  2. 互斥鎖
    2.1 pthread_mutex_t
    2.2 NSLock
    2.3 NSRecursiveLock
    2.4 @synchronized
    2.5 dispatch_semaphore_t
    2.6 NSCondition
    2.7 NSConditionLock
  3. 讀寫鎖

性能對比
參考


概念

什么是鎖
  • 鎖是一種同步的機(jī)制。
  • 鎖是一個對象。
  • 鎖是為了保證某一個資源在同一時間,不被多個“潛在調(diào)用者”持有,保證資源在同一時間不會因搶奪而出現(xiàn)錯誤。
  • 鎖是對資源的訪問限制。


死鎖

當(dāng)兩個及兩個以上的運(yùn)算單元在等待對方停止運(yùn)行,從而獲取對方持有的系統(tǒng)資源,但又沒有一方提前退出爭奪的時候,就會產(chǎn)生死鎖。


死鎖

如果線程1和線程2,誰都不先釋放自己對已擁有的鎖對象的持有權(quán),那么就會陷入互相等待對方先松手的狀態(tài),這就是死鎖。

死鎖產(chǎn)生的必要條件
  1. 互斥條件 :
    資源只能在同一時間分配給某一個運(yùn)算單元,如果其他的運(yùn)算單元也要請求同一資源,則只能等待持有資源的運(yùn)算單元使用完畢。
  2. 持有和等待 :
    某運(yùn)算單元已經(jīng)持有一個或多個資源,又請求了其他被占用的資源,這個運(yùn)算單元并不釋放自己已有的資源,持有資源進(jìn)行新資源的等待。
  3. 不可剝奪條件 :
    指運(yùn)算單元已經(jīng)獲得了資源,在沒有使用完成該資源的情況下,該資源不可以被剝奪,只能等待運(yùn)算單元使用完畢后自己釋放。
  4. 循環(huán)等待 :
    指一組集合中有很多運(yùn)算單元,它們互相持有其他運(yùn)算單元的資源。

1. 自旋鎖

線程反復(fù)檢查鎖變量是否可用,在此過程中線程一直處于執(zhí)行狀態(tài)。適用于預(yù)期持有鎖時間很短的操作,此時因?yàn)樽枞€程和喚醒涉及上下文切換和線程數(shù)據(jù)結(jié)構(gòu)的更新,在cpu資源寬裕且鎖預(yù)期等待時間很短的情況下,輪詢通常比阻塞線程更有效。

1.1 OSSpinLock(iOS 10 以后廢棄)

OSSpinLock spinLock = OS_SPINKLOCK_INIT;
OSSpinLockLock(&spinLock);
//code
OSSpinLockUnlock(&spinLock);

OSSSpinLock 存在優(yōu)先級反轉(zhuǎn)問題。如果一個低優(yōu)先級的線程 A 獲得鎖并訪問共享資源,這時如果另一個高優(yōu)先級的線程 B 也嘗試獲得這個鎖,線程 B 會處于忙等狀態(tài),由于線程 B 是一個高優(yōu)先級線程,因此 CPU 會盡量將執(zhí)行的資源分配給線程B,從而導(dǎo)致線程 B 占用大量 CPU 而線程 A 由于得到的執(zhí)行資源少而遲遲無法解鎖。

1.2 os_unfair_lock

//頭文件
@import Darwin.os.lock;

os_unfair_lock_t lock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(lock);
// code
os_unfair_lock_unlock(lock);

多個線程同時等待鎖時,先請求獲取鎖的線程不一定會先獲取鎖,鎖的獲取與請求鎖的先后順序無關(guān),例如最后請求獲取鎖的線程可能先獲得鎖。
鎖的獲取和釋放基于原子操作。
只包含一個指針大小的內(nèi)存空間,性能開銷小。

1.3 atomic

@protocol(atomic, assign) NSInteger count;

編譯器自動生成 getter / setter 內(nèi)部會調(diào)用 objc_getProperty / reallySetProperty 方法,方法內(nèi)部根據(jù)是否為原子屬性執(zhí)行不同的代碼

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

原子性修飾的屬性會進(jìn)行 spinlock 加鎖處理,spinlock由于優(yōu)先級反轉(zhuǎn)問題已經(jīng)被 os_unfair_lock 代替

using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
    os_unfair_lock mLock;
    ...
}

對于原子性修飾的屬性,只能保證 getter / setter 的線程安全,無法保證屬性在使用過程中的線程安全。例如可變數(shù)組在多個線程中 removeObjectAtIndex:


2. 互斥鎖

是一種用于多線程編程中,防止多條線程同時對同一公共資源(比如全局變量)進(jìn)行讀寫的機(jī)制。它通過將代碼切片成一個一個的臨界區(qū)域達(dá)成。臨界區(qū)域指的是一塊對公共資源進(jìn)行訪問的代碼,并非一種機(jī)制或是算法。一個程序、進(jìn)程、線程可以擁有多個臨界區(qū)域,但是并不一定會應(yīng)用互斥鎖。
互斥鎖不會出現(xiàn)忙碌等待,僅僅是線程阻塞。

2.1 pthread_mutex_t

// 導(dǎo)入頭文件
#import <pthread/pthread.h>

pthread_mutex_t lock;
pthread_mutexattr_t attr;
// 初始化屬性
pthread_mutexattr_init(&attr);
/*
 * Mutex type attributes
#define PTHREAD_MUTEX_NORMAL        0  // 普通
#define PTHREAD_MUTEX_ERRORCHECK    1  // 此類型互斥量會自動檢測死鎖。檢查錯誤、提供錯誤提示,需要消耗一定的性能
#define PTHREAD_MUTEX_RECURSIVE     2  // 遞歸
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL
 */
// 設(shè)置類型為遞歸
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化鎖,如果不需要設(shè)置屬性則第二個參數(shù)傳入NULL,使用默認(rèn)屬性
pthread_mutex_init(&lock, &attr);
// 銷毀屬性
pthread_mutexattr_destroy(&attr);
// 加鎖
pthread_mutex_lock(&lock);
// code
// 解鎖
pthread_mutex_unlock(&lock);

// 使用完成后需要在合適的時機(jī)對鎖進(jìn)行銷毀
pthread_mutex_destroy(&lock);

pthread_mutex 是 iOS 中,多種類型的鎖的底層實(shí)現(xiàn),例如 NSLock、NSRecursiveLock、@ synchronized 。
pthread_mutex底層有實(shí)現(xiàn)一個阻塞隊(duì)列,如果當(dāng)前有其他任務(wù)正在執(zhí)行,則加入到隊(duì)列中,放棄當(dāng)前cpu時間片。一旦其他任務(wù)執(zhí)行完,則從隊(duì)列中取出等待執(zhí)行的線程對象,恢復(fù)上下文重新執(zhí)行。

2.2 NSLock
NSLock * lock = [[NSLock alloc] init];
[lock lock];
// code
[lock unlock];

NSLock 是對 pthread_mutex 的封裝,屬性為 PTHREAD_MUTEX_ERRORCHECK,它會自動檢測死鎖,損失一定性能換來錯誤提示。
注意:在同一個線程中多次對同一個對象加鎖會導(dǎo)致死鎖。

2.3 NSRecursiveLock
NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];
[lock lock];
// code 
[lock unlock];

NSRecursiveLock 是對 pthread_mutex 的封裝,屬性為 PTHREAD_MUTEX_RECURSIVE
遞歸鎖允許在同一線程內(nèi)對同一個鎖對象多次加鎖,但是需要注意的是在線程執(zhí)行完畢后必須在當(dāng)前線程內(nèi)進(jìn)行同樣次數(shù)的解鎖操作,否則會導(dǎo)致其他線程無法獲得鎖(死鎖)。

- (void)NSRecursiveLockTest:(NSRecursiveLock *)lock some:(NSInteger)i {
    [lock lock];
    NSLog(@"%zd", i);
    if (i != 0) {
        [self NSRecursiveLockTest:lock some:--i];
    } else {
        // i == 0 時直接結(jié)束遞歸,不進(jìn)行解鎖
        return;
    }
    [lock unlock];
}

NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];
// 由于首先執(zhí)行的線程沒有解鎖,導(dǎo)致后面執(zhí)行的線程一直在等待解鎖
dispatch_async(self.queue, ^{
     [self NSRecursiveLockTest:lock some:3];
});
dispatch_async(self.queue, ^{
     [self NSRecursiveLockTest:lock some:4];
});
2.4 @synchronized
@synchronized (obj) {
// code
}

@synchronized是對pthread_mutex遞歸鎖的封裝, @synchronized(obj)內(nèi)部會生成obj對應(yīng)的遞歸鎖,然后進(jìn)行加鎖、解鎖操作。如果 obj 為 nil 則不會進(jìn)行加鎖,不能保證線程安全。

2.5 dispatch_semaphore_t
// 初始化 
dispatch_semaphore_t semaphore_t = dispatch_semaphore_create(0);
// 如果信號計數(shù) <= 0 阻塞當(dāng)前線程,否則信號計數(shù) - 1 不阻塞當(dāng)前線程
dispatch_semaphore_wait(semaphore_t,DISPATCH_TIME_FOREVER);
// 信號計數(shù) + 1
dispatch_semaphore_signal(semaphore_t);

GCD信號量是通過對信號量計數(shù)和0的對比來進(jìn)行鎖的實(shí)現(xiàn)。

  • 當(dāng)信號量的信號計數(shù) > 0,使信號計數(shù) -1,并不造成線程阻塞。
  • 當(dāng)信號量的信號計數(shù) <= 0,其所在的線程會被阻塞執(zhí)行,直到信號計數(shù) > 0 為止。

信號量除了可以作為鎖使用還可以用于將異步操作轉(zhuǎn)為同步操作。

- (void)fetchDataComplete:(void(^)(id data, BOOL isSuccess))complete {
    // 模擬網(wǎng)絡(luò)請求
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), self.queue, ^{
        NSArray * data = @[@"data"];
        complete(data, YES);
    });
}

- (id)syncFetchData {
    // 1.創(chuàng)建信號量,計數(shù)為0
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    __block id data = nil;
    [self fetchDataComplete:^(id d, BOOL isSuccess) {
        if (isSuccess) {
            data = d;
        }
        // 3.數(shù)據(jù)請求完成,發(fā)送 signal 信號計數(shù) + 1 繼續(xù)執(zhí)行 2
        dispatch_semaphore_signal(semaphore);
    }];
    // 2.計數(shù) <= 0 阻塞當(dāng)前線程,等待異步請求回調(diào)
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    return data;
}
2.6 NSCondition
NSCondition * condition = [[NSCondition alloc] init];
// 加鎖
[condition lock];
// 解鎖
[condition unlock];

NSLock 基于 mutex 與 POSIX condition 實(shí)現(xiàn),除了基礎(chǔ)的 lock / unlock 還支持類似于 信號量 功能:

  • wait 釋放互斥量,線程進(jìn)入休眠狀態(tài)。
  • waitUntilDate: 釋放互斥量,當(dāng)前線程立即進(jìn)入休眠,其他線程繼續(xù)執(zhí)行任務(wù),直到limit時間點(diǎn),當(dāng)前線程再被喚醒。
  • signal 喚醒一個等待的線程
  • broadcast 喚醒所有等待的線程

以上方法必須在 NSCondition 對象 lock 之后調(diào)用,例如下面的例子:

// 初始有兩張票
static NSInteger ticket = 2;
static NSCondition * condition;
- (void)NSConditionTest {
    condition = [[NSCondition alloc] init];
    for (int i = 0; i < 5; i++) {
        dispatch_async(self.queue, ^{
            [self waitTicket];
        });
    }
    for (int i = 0; i < 3; i++) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * 3 * NSEC_PER_SEC)), self.queue, ^{
            [self signalTicket];
        });
    }
}

- (void)waitTicket {
    // 加鎖
    [condition lock];
    while (!ticket) {
        NSLog(@"沒有票,線程休眠");
        // 釋放互斥量,線程休眠
        [condition wait];
        // 線程喚醒后繼續(xù)while循環(huán)檢查票數(shù),如果沒有票說明票已經(jīng)被之前喚醒的線程售出,當(dāng)前線程再次休眠
        NSLog(@"線程喚醒,檢查票數(shù)");
    }
    ticket--;
    NSLog(@"賣出了一張票 剩余:%zd", ticket);
    // 解鎖
    [condition unlock];
}

- (void)signalTicket {
    // 加鎖
    [condition lock];
    ticket++;
    NSLog(@"發(fā)行了一張票 剩余:%zd", ticket);
    // 發(fā)送信號,通知喚醒一條休眠的線程
    [condition signal];
    // 解鎖
    [condition unlock];
}

賣出了一張票 剩余:1
賣出了一張票 剩余:0
沒有票,線程休眠
沒有票,線程休眠
沒有票,線程休眠
發(fā)行了一張票 剩余:1
線程喚醒,檢查票數(shù)
賣出了一張票 剩余:0
發(fā)行了一張票 剩余:1
線程喚醒,檢查票數(shù)
賣出了一張票 剩余:0
發(fā)行了一張票 剩余:1
線程喚醒,檢查票數(shù)
賣出了一張票 剩余:0

如果將以上例子用 mutex 與 POSIX condition 實(shí)現(xiàn)如下:

static pthread_mutex_t mutex;
static pthread_cond_t cond;
- (void)mutexAndCondTest {
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);
    for (int i = 0; i < 5; i++) {
        dispatch_async(self.queue, ^{
            [self cond_waitTicket];
        });
    }
    for (int i = 0; i < 3; i++) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * 3 * NSEC_PER_SEC)), self.queue, ^{
            [self cond_signalTicket];
        });
    }
}

- (void)cond_waitTicket {
    pthread_mutex_lock(&mutex);
    while (!ticket) {
        NSLog(@"沒有票,線程休眠");
        pthread_cond_wait(&cond, &mutex);
        NSLog(@"線程喚醒,檢查票數(shù)");
    }
    ticket--;
    NSLog(@"賣出了一張票 剩余:%zd", ticket);
    pthread_mutex_unlock(&mutex);
}

- (void)cond_signalTicket {
    pthread_mutex_lock(&mutex);
    ticket++;
    NSLog(@"發(fā)行了一張票 剩余:%zd", ticket);
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
}
2.7 NSConditionLock
// 初始化并設(shè)置條件變量
NSConditionLock * conditionLock = [[NSConditionLock alloc] initWithCondition:cond];
// 當(dāng)條件變量相等且可以獲取到鎖時加鎖,否則阻塞
[conditionLock lockWhenCondition:cond];
// 只要能夠獲取到鎖就加鎖,否則阻塞
// [conditionLock lock];
// code
// 解鎖并重新設(shè)置條件變量
[conditionLock unlockWithCondition:cond];
// 解鎖 不會改變當(dāng)前條件變量
// [conditionLock unlock];

NSConditionLock 是對 NSCondition 的封裝,在加鎖前會比較等待變量和條件變量是否相等,如果不相等則阻塞線程

    // 初始化條件變量
    NSConditionLock * lock = [[NSConditionLock alloc] initWithCondition:2];
    dispatch_async(self.queue, ^{
        // 條件變量為 1 且能夠獲取到鎖時加鎖,否則阻塞
        [lock lockWhenCondition:1];
        NSLog(@"1");
        // 解鎖并將條件變量設(shè)置為 2
        [lock unlockWithCondition:2];
    });
    dispatch_async(self.queue, ^{
        // 條件變量為 3 且能夠獲取到鎖時時加鎖,否則阻塞
        [lock lockWhenCondition:3];
        NSLog(@"2");
        // 解鎖并將條件變量設(shè)置為 1
        [lock unlockWithCondition:1];
    });
    dispatch_async(self.queue, ^{
        // 無論條件變量為多少只要獲取到鎖就進(jìn)行加鎖,如果獲取不到鎖則阻塞線程
        [lock lock];
        NSLog(@"4");
        // 解鎖且不改變條件變量
        [lock unlock];
    });
    dispatch_async(self.queue, ^{
        // 條件變量為 2 且能夠獲取到鎖時時加鎖,否則阻塞
        [lock lockWhenCondition:2];
        NSLog(@"3");
        // 解鎖并將條件變量設(shè)置為 3
        [lock unlockWithCondition:3];
    });

執(zhí)行順序: 4 -> 3 -> 2 -> 1

3. 讀寫鎖

讀寫鎖實(shí)際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進(jìn)行讀訪問,寫者則需要對共享資源進(jìn)行寫操作。這種鎖相對于自旋鎖而言,能提高并發(fā)性,因?yàn)樵诙嗵幚砥飨到y(tǒng)中,它允許同時有多個讀者來訪問共享資源,最大可能的讀者數(shù)為實(shí)際的CPU數(shù)
寫者是排他性的,?個讀寫鎖同時只能有?個寫者或多個讀者(與CPU數(shù)相關(guān)),但不能同時既有讀者?有寫者。在讀寫鎖保持期間也是搶占失效的
如果讀寫鎖當(dāng)前沒有讀者,也沒有寫者,那么寫者可以?刻獲得讀寫鎖,否則它必須?旋在那?,直到?jīng)]有任何寫者或讀者。如果讀寫鎖沒有寫者,那么讀者可以?即獲得該讀寫鎖,否則讀者必須?旋在那?,直到寫者釋放該讀寫鎖。

pthread_rwlock_t

// 導(dǎo)入頭文件
#import <pthread/pthread.h>

pthread_rwlock_t lock;
// 初始化讀寫鎖
pthread_rwlock_init(&lock, NULL);

// 讀操作-加鎖
pthread_rwlock_rdlock(&lock);
// 讀操作-嘗試加鎖
pthread_rwlock_tryrdlock(&lock);
// 寫操作-加鎖
pthread_rwlock_wrlock(&lock);
// 寫操作-嘗試加鎖
pthread_rwlock_trywrlock(&lock);
// 解鎖
pthread_rwlock_unlock(&lock);
// 銷毀鎖
pthread_rwlock_destroy(&lock);

讀寫鎖的三種狀態(tài) :

  • 以讀的方式占據(jù)鎖的狀態(tài) :
    如果有其他的線程以讀的方式請求占據(jù)鎖,并讀取鎖內(nèi)的共享資源,不會造成線程阻塞,允許其他線程進(jìn)行讀取,就像遞歸鎖的可重入一樣。
    如果有其他的線程以寫的方式請求占據(jù)鎖,企圖更改鎖內(nèi)的共享資源,則會阻塞請求的線程,直到讀的操作進(jìn)行完畢。
    如果有其他多條線程,分別以讀和寫的不同方式請求占據(jù)鎖,那么這些多條線程也會被阻塞,并且在當(dāng)前線程讀操作結(jié)束后,先讓寫方式的線程占據(jù)鎖,避免讀模式的鎖長期占用資源,而寫模式的鎖卻長期堵塞。
  • 以寫的方式占據(jù)鎖的狀態(tài) : 所有其他請求占據(jù)鎖的線程都會阻塞。
  • 沒有線程占據(jù)鎖的狀態(tài) : 按照操作系統(tǒng)的調(diào)度順序,依次調(diào)用,調(diào)度后要符合上述兩種情況。

性能對比

性能

參考

Threading Programming Guide
【iOS】—— iOS中的相關(guān)鎖
iOS線程安全——鎖
iOS 多線程下的不同鎖
iOS常用的幾種鎖詳解以及用法
第三十二節(jié)—iOS的鎖(一)
iOS GCD (四) dispatch_semaphore 信號量

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. 為什么多線程需要鎖? 首先在多線程處理的時候我們經(jīng)常會需要保證同步,這是為啥呢,看一下下面這個例子: 這種時...
    木小易Ying閱讀 1,165評論 0 8
  • 前言 最開始我想把線程和線程鎖放在一起整理出一篇文章,結(jié)果整理了線程發(fā)現(xiàn)有點(diǎn)長,于是便把線程鎖單獨(dú)拿出來了。感興趣...
    陌路賣醬油閱讀 1,101評論 0 21
  • 一、線程分享梗概 二、線程的概念和實(shí)現(xiàn) 線程:是程序執(zhí)行流的最小單元。一個標(biāo)準(zhǔn)的線程由線程ID,當(dāng)前指令集合,寄存...
    魁拔2015閱讀 5,770評論 0 22
  • 一、線程鎖相關(guān)概念 線程鎖:我們在使用多線程的時候多個線程可能會訪問同一塊資源,這樣就很容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全...
    2525252472閱讀 492評論 0 2
  • 多線程系列篇章計劃內(nèi)容:iOS多線程編程(一) 多線程基礎(chǔ)[https://juejin.im/post/6890...
    賣饃工程師閱讀 971評論 0 3

友情鏈接更多精彩內(nèi)容