線程鎖


線程鎖

是為了解決多個線程之間共享同一資源時,對資源的占用控制,防止多個線程之間同時修改同一資源信息,導致不可預知的問題。

鎖的實現方式大致可以分為以下兩種:

  • 阻塞
  • 忙等

阻塞:如果鎖對象被其他線程所持有,那么請求訪問的線程就會被加入到等待隊列中,因而被阻塞。這就意味着被阻塞的線程放棄了時間片,調度器會將CPU讓給下一個執行的的線程。當鎖可用的時候,調度器會得到通知,然后根據情況將線程從等待隊列取出來,並重新調度。

忙等:線程不放棄CPU時間片,而是繼續重復的嘗試訪問鎖,直到鎖可用。
 
iOS開發中包括以下鎖:
 
OSSpinLock(自旋鎖)
os_unfair_lock(互斥鎖,iOS10之后,用於替代OSSpinLock的鎖)
dispatch_semaphore_t(信號量)
pthread_mutex(互斥鎖)
NSLock(互斥鎖,對pthread_mutex互斥鎖的OC封裝)
pthread_mutex(遞歸鎖)
NSRecursiveLock(遞歸鎖,對pthread_mutex遞歸鎖的OC封裝)
NSCondition(條件鎖)
NSConditionLock(條件鎖,在NSCondition基礎上實現的)
@synchronized(遞歸鎖):其本質是調用objc_sync_enter和objc_sync_exit
Atomic(原子操作)
pthread_rwlock_t(讀寫鎖)
GCD柵欄(可以用於讀寫鎖)
 
 

OSSpinLock(自旋鎖)

自旋鎖與互斥鎖有點類似,只是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那里看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名。 由於自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高於互斥鎖。適用於處理耗時較短的任務。
新版 iOS 中,系統維護了 5 個不同的線程優先級/QoS: backgroundutilitydefaultuser-initiateduser-interactive。高優先級線程始終會在低優先級線程前執行,一個線程不會受到比它更低優先級線程的干擾。這種線程調度算法會產生潛在的優先級反轉問題,從而破壞了 spin lock
因此蘋果在iOS10的時候廢棄了OSSpinLock,采用os_unfair_lock代替。
#import <libkern/OSAtomic.h>
@interface DrLock : NSObject {
    
    OSSpinLock _lock;
}

- (void)lock;
- (int)tryLock;
- (void)unlock;

@end
@implementation DrLock

- (instancetype)init{
    self = [super init];
    if (self) {
        _lock = OS_SPINLOCK_INIT;
    }
    return self;
}

/// 上鎖
- (void)lock {
    OSSpinLockLock(&_lock);
}

/// 上鎖,失敗返回:NO,成功返回:YES
- (int)tryLock {
    return OSSpinLockTry(&_lock);
}

/// 解鎖
- (void)unlock {
    OSSpinLockUnlock(&_lock);
}

@end

關於tryLock的使用,當返回NO時,表示當前鎖已經被鎖住(即:有一個線程在訪問邊界值時被當前鎖鎖住了),此時我們需要調用lock將新的線程鎖住。因此我們可以通過這個方法來判斷當前是否存在多個線程同時訪問邊界值。

 

os_unfair_lock(互斥鎖)

os_unfair_lock是用於替代OSSpinLock的,與OSSpinLock的不同點在於等待鎖的線程不會處於忙等狀態,而是阻塞當前線程,使其處於休眠狀態。在使用方面與OSSpinLock一樣,代碼如下:
#import <os/lock.h>
@interface DrLock : NSObject {
    
    os_unfair_lock _lock;
}

- (void)lock;
- (int)tryLock;
- (void)unlock;

@end
@implementation DrLock

- (instancetype)init{
    self = [super init];
    if (self) {
        _lock = OS_UNFAIR_LOCK_INIT;
    }
    return self;
}

/// 上鎖
- (void)lock {
    os_unfair_lock_lock(&_lock);
}

/// 上鎖,失敗返回:NO,成功返回:YES
- (int)tryLock {
    return os_unfair_lock_trylock(&_lock);
}

/// 解鎖
- (void)unlock {
    os_unfair_lock_unlock(&_lock);
}

@end

關於tryLock的使用,當返回NO時,表示當前鎖已經被鎖住(即:有一個線程在訪問邊界值時被當前鎖鎖住了),此時我們需要調用lock將新的線程鎖住。因此我們可以通過這個方法來判斷當前是否存在多個線程同時訪問邊界值。

 

dispatch_semaphore_t(信號量)

信號量是通過維護一個值a,初始化時設置這個值a,當調用wait方法時,使a-=1,此時如果a<=0,當前線程將被阻塞,至於休眠狀態。當調用signal方法時,使a+=1,此時如果a>0或wait設置的timeout時間到了,被阻塞的線程會被喚起,繼續處理任務。因此在處理並發任務時,我們可以通過初始化時設置一個值,來控制並發的個數。
@interface DrLock : NSObject {
    
    dispatch_semaphore_t _lock;
}

- (void)lock;
- (void)unlock;

@end
@implementation DrLock

- (instancetype)init{
    self = [super init];
    if (self) {
        _lock = dispatch_semaphore_create(1);
    }
    return self;
}

/// 上鎖
- (void)lock {
    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
}

/// 解鎖
- (void)unlock {
    dispatch_semaphore_signal(_lock);
}

@end

 

 

pthread_mutex(互斥鎖)

pthread_mutex的使用和os_unfair_lock差不多,唯一不同就是需要提供屬性設置,這里attr有幾種type可以設置,包括: PTHREAD_MUTEX_NORMALPTHREAD_MUTEX_ERRORCHECKPTHREAD_MUTEX_RECURSIVE,一般就使用PTHREAD_MUTEX_NORMAL就可以了。
在使用pthread_mutex時需要注意,在不需要這個鎖的時候,需要手動釋放鎖資源
#import <pthread.h>
@interface DrLock : NSObject {
    
    pthread_mutex_t _lock;
}

- (void)lock;
- (int)tryLock;
- (void)unlock;

@end
@implementation DrLock

- (void)dealloc{
    pthread_mutex_destroy(&_lock); // 注意這里要銷毀鎖
}

- (instancetype)init{
    self = [super init];
    if (self) {
//        pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 靜態初始化方法
//        _lock = lock;
        
        // 動態初始化
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 設置鎖的類型
        pthread_mutex_init(&_lock, &attr);
        pthread_mutexattr_destroy(&attr);
    }
    return self;
}

/// 上鎖
- (void)lock {
    pthread_mutex_lock(&_lock);
}

/// 上鎖,返回值==0:成功;返回值!=0:失敗(當互斥量已經被鎖住時調用該函數將返回錯誤代碼EBUSY:16,此時需要調用lock對當前線程上鎖)
- (int)tryLock {
    return pthread_mutex_trylock(&_lock);
}

/// 解鎖
- (void)unlock {
    pthread_mutex_unlock(&_lock);
}

@end

關於tryLock的使用,當返回非0的值時,表示當前鎖已經被鎖住(即:有一個線程在訪問邊界值時被當前鎖鎖住了),此時我們需要調用lock將新的線程鎖住。因此我們可以通過這個方法來判斷當前是否存在多個線程同時訪問邊界值。

 
- (void)task:(int)ID {
    int res = [_lock tryLock];
    if (res != 0) {
        NSLog(@"加鎖失敗: %@", @(res));
        [_lock lock];
    }
    NSLog(@"執行任務:%@", @(ID));
    sleep(2);
    NSLog(@"執行任務:%@完成", @(ID));
    [_lock unlock];
}

 

pthread_mutex(遞歸鎖) 

它與互斥鎖類似,都是采用阻塞當前線程的方式實現加鎖功能,區別在於它可以保證同一個線程可以多次進入同一塊加鎖區域一般情況下。一個線程只能申請一次鎖,也只能在獲得鎖的情況下才能釋放鎖,多次申請鎖或釋放未獲得的鎖都會導致崩潰。假設在已經獲得鎖的情況下再次申請鎖,線程會因為等待鎖的釋放而進入睡眠狀態,因此就不可能再釋放鎖,從而導致死鎖。

然而這種情況經常會發生,比如某個函數申請了鎖,在臨界區內又遞歸調用了自己(出現於遞歸調用的情況)。因此我們需要一個遞歸鎖解決這種情況的調用。

#import <pthread.h>
@interface DrLock : NSObject {
    
    pthread_mutex_t _lock;
}

- (void)lock;
- (int)tryLock;
- (void)unlock;

@end
@implementation DrLock

- (void)dealloc{
    pthread_mutex_destroy(&_lock); // 注意這里要銷毀鎖
}

- (instancetype)init{
    self = [super init];
    if (self) {
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 這里設置的類型為遞歸鎖
        pthread_mutex_init(&_lock, &attr);
        pthread_mutexattr_destroy(&attr);
    }
    return self;
}

/// 上鎖
- (void)lock {
    pthread_mutex_lock(&_lock);
}

/// 上鎖,返回值==0:成功;返回值!=0:失敗(當互斥量已經被鎖住時調用該函數將返回錯誤代碼EBUSY:16,此時需要調用lock對當前線程上鎖)
- (int)tryLock {
    return pthread_mutex_trylock(&_lock);
}

/// 解鎖
- (void)unlock {
    pthread_mutex_unlock(&_lock);
}

@end

遞歸鎖與互斥鎖的區別,僅僅在於設置的類型不一樣,遞歸鎖的類型為:PTHREAD_MUTEX_RECURSIVE。下面我們舉個例子,介紹一下這個遞歸鎖的用法。

下面是一個倒計時的任務,多個線程會調用該方法,每個線程調用時傳入的參數不同。
- (void)task:(int)ID count:(int)count {
    int i = [_lock tryLock];
    if (i != 0) {
        NSLog(@"加鎖失敗: %@", @(i));
        [_lock lock];
    }
    if (count == 0) {
        NSLog(@"執行任務:%@完成", @(ID));
        sleep(1);
        [_lock unlock];
        return;
    }
    NSLog(@"執行任務:%@,倒計時:%@", @(ID), @(count));
    [self task:ID count:count-1];
    [_lock unlock];
}

需要注意的一點,lock與unlock一定要成對執行,即遞歸前后,需要保持lock與unlock的調用次數一致,否則會出現未解鎖的情況,導致線程一直處於阻塞狀態,后續的任務無法進行。

當然遞歸鎖也可以用於非遞歸的調用方法中,只要確保lock與unlock調用次數一致即可。而在遞歸調用的方法中我們必須要使用遞歸鎖了。

 

NSLock(互斥鎖)

是對pthread_mutex(互斥鎖)OC封裝,其內部維護了一個pthread_mutex互斥鎖,用法和pthread_mutex的使用一樣。並且它提供了一個特殊的lock方法

- (BOOL)lockBeforeDate:(NSDate *)limit;

該方法用於指定一個時間,在該時間之前會對當前線程一直執行加鎖。當超出指定時間后未收到unlock,會返回NO,加鎖失敗,相當於給當前鎖設置了一個超時時間。
- (void)task:(int)ID {
    if (![_lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:2]]) {
        NSLog(@"加鎖失敗");
        [_lock lock];
    }
    NSLog(@"執行任務:%@", @(ID));
    sleep(2);
    NSLog(@"執行任務:%@完成", @(ID));
    [_lock unlock];
}

以上任務會執行2秒鍾,我們在加鎖的地方采用lockBeforeDate方法,設置一個鎖的超時時間2秒,當2秒后未收到unlock,該方法將不再阻塞線程,返回NO,因此我們增加了一個lock方法,防止多個線程同時調用該方法,導致lockBeforeDate超過2秒。

以上代碼一旦同時存在兩個或以上線程同時調用,就會出現lockBeforeDate超時,導致加鎖失敗。

 

 NSRecursiveLock(遞歸鎖)

 是對pthread_mutex(遞歸鎖)的OC封裝,其內部維護了一個pthread_mutex遞歸鎖,用法與pthread_mutex一樣。同樣它也提供了一個與NSLock一樣的方法

- (BOOL)lockBeforeDate:(NSDate *)limit;

 它的作用於NSLock的一樣。
 
 

NSCondition(條件鎖)

它實際上既是一個鎖對象,又是一個線程檢查器:鎖主要為了當檢測條件時保護數據源,執行條件引發的任務;線程檢查器主要是根據條件決定是否繼續運行線程,即線程是否被阻塞。通俗的說,也就是條件成立,才會執行鎖住的代碼。條件不成立時,線程就會阻塞,直到另一個線程向條件對象發出信號解鎖為止。
它除了鎖必須的一些方法外,還提供了如下的方法,用於阻塞線程與喚起線程
- (void)wait; // 阻塞線程
- (BOOL)waitUntilDate:(NSDate *)limit; // 阻塞線程,並設置一個超時時間
- (void)signal; // 喚醒一個阻塞中的線程
- (void)broadcast; // 喚醒全部阻塞中的線程

 該鎖適用於生產者與消費者模式,例如:一個公司負責生產商品,一些消費者負責購買這些商品

 

/// 生產商品
- (void)production:(NSString *)goodsName {
    [_condition lock];
    [_goodsList addObject:goodsName];
    [_condition unlock];
//    [_condition signal]; // 通知一個正在排隊的消費者,有貨了,可以購買了
    [_condition broadcast]; // 通知所有正在排隊的消費者,有貨了,可以購買了
}

/// 購買商品
- (void)buy {
    
    [_condition lock];
    while (_goodsList.count == 0) {
        NSLog(@"%@ 正在排隊......", [NSThread currentThread].name);
        // 沒商品了,排隊等待
//        [_condition wait];// 這里會讓當前線程阻塞,並休眠,直到發出signal或broadcast被喚醒,繼續向下執行
//        if (![_condition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:6]]) { // 最多等6秒,我就不等了
//            break; // 等待超時waitUntilDate會返回NO,而通過signal或broadcast喚醒,它會返回YES。因此如果是超時了,那就break跳出循環,向下執行
//        }
    }
    
    if (_goodsList.count == 0) {
        NSLog(@"%@ 沒有買到商品,回家了......", [NSThread currentThread].name);
    }else {
        
        NSString *goods = [_goodsList lastObject];
        [_goodsList removeLastObject];
        NSLog(@"%@ 買到了 %@", [NSThread currentThread].name, goods);
    }
    [_condition unlock];
}



    // 消費者購買商品
    
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(buy) object:nil];
    thread1.name = @"jack";
    [thread1 start];
    
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(buy) object:nil];
    thread2.name = @"bob";
    [thread2 start];
    
    NSThread *thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(buy) object:nil];
    thread3.name = @"drbox";
    [thread3 start];
    
    
    // 工人生產商品
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        dispatch_queue_t queue = dispatch_queue_create("workers1", DISPATCH_QUEUE_SERIAL);
        dispatch_async(queue, ^{
            [self production:@"蘋果iPhoneX 編號:1001"];
        });
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        dispatch_queue_t queue = dispatch_queue_create("workers2", DISPATCH_QUEUE_SERIAL);
        dispatch_async(queue, ^{
            [self production:@"蘋果iPhoneX 編號:1002"];
        });
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        dispatch_queue_t queue = dispatch_queue_create("workers3", DISPATCH_QUEUE_SERIAL);
        dispatch_async(queue, ^{
            [self production:@"蘋果iPhoneX 編號:1003"];
        });
    });

 

以上為典型的生產者與消費者模式,當生產者生產出一件商品,通知一個或全部排隊中的消費者購買。注意,這里有兩點注意事項:

  1. 由於生產商品為多線程,因此需要對goodsList操作前加鎖。
  2. 通知消費者時調用signalbroadcast的不同,signal只會喚起一個休眠中的線程,而broadcast則會喚起全部休眠中的線程,因此我們在buy方法中增加了一個while循環來判斷,防止broadcast喚起全部休眠的線程,導致有些線程錯誤的處理產品是否為空的情況
 在消費者購買的 buy方法中,當前沒有商品時,調用wait或waitUntilDate讓當前線程休眠,休眠后,wait或waitUntilDate后面的代碼將不會執行,直到被喚醒或等待超時,才會繼續執行后面的代碼。 注意:這里在處理超時的情況判斷,只有當線程通過signal或broadcast被喚醒的情況,waitUntilDate才會返回YES,否則返回NO
 
 

pthread_mutex_t + pthread_cond_t(實現條件鎖)

通過pthread_mutex_t與pthread_cond_t組合,可以實現與 NSCondition一樣的效果 ,實際上NSCondition底層就是通過這兩個組合實現的,NSCondition就是對這兩組合的OC封裝,具體的代碼實現如下:
#import <pthread.h>
@interface DrLock : NSObject {
    
    pthread_mutex_t _lock;
    pthread_cond_t _cond; // 條件
}

- (void)lock;
- (void)unlock;

@end
@implementation DrLock

- (void)dealloc{
    pthread_mutex_destroy(&_lock); // 注意這里要銷毀鎖
    pthread_cond_destroy(&_cond);
}

- (instancetype)init{
    self = [super init];
    if (self) {
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT); // 這里設置的類型為互斥鎖
        pthread_mutex_init(&_lock, &attr);
        pthread_mutexattr_destroy(&attr);
        
        pthread_condattr_t conAttr;
        pthread_condattr_init(&conAttr);
        
        pthread_condattr_setpshared(&conAttr, PTHREAD_PROCESS_PRIVATE);
        pthread_cond_init(&_cond, &conAttr);
    }
    return self;
}

/// 上鎖
- (void)lock {
    pthread_mutex_lock(&_lock);
}

/// 解鎖
- (void)unlock {
    pthread_mutex_unlock(&_lock);
}

/// 阻塞當前線程
- (void)wait {
    pthread_cond_wait(&_cond, &_lock);
}

/// 阻塞當前線程
- (BOOL)waitUntilDate:(NSDate *)date {
    struct timespec time;
    time.tv_sec = date.timeIntervalSince1970;
    time.tv_nsec = 0; // 這里必須設置一個數,否則計時不起作用,具體設置多少我不知道這里如何設置,有清楚的可以回復我,謝謝!
    return pthread_cond_timedwait(&_cond, &_lock, &time) == 0;
}

/// 喚醒一個被阻塞的線程
- (void)signal {
    pthread_cond_signal(&_cond);
}

/// 喚醒全部阻塞中的線程
- (void)broadcast {
    pthread_cond_broadcast(&_cond);
}

@end

 

這里我們用到了一個pthread_cond_t條件(記得需要手動釋放資源),它在初始化時,需要設置一個屬性值,這里我們設置成PTHREAD_PROCESS_PRIVATE,實際上這個也是缺省值。

關於pthread_condattr_setpshared的值范圍有以下說明:

 因為pthread_mutex屬於 POSIX 線程下的互斥鎖,因此它的取值包括:PTHREAD_PROCESS_SHAREDPTHREAD_PROCESS_PRIVATE

因為在客戶端上使用條件鎖處理的線程,大部分都是在同一個進程下創建的,因此我們這里使用 PTHREAD_PROCESS_PRIVATE
 
 

NSConditionLock(升級版的條件鎖)

該鎖可以使得互斥鎖在某種條件下進行鎖定和解鎖,它和 NSCondition很像,但實現方式不同。它沒有wait與signal等方法讓線程休眠與喚醒,它是通過指定條件,讓某個線程來獲得鎖。我們依然使用NSCondition中的例子來了解它的使用,我們只需要改動生產商品和購買商品這兩個方法即可:
/// 生產商品
- (void)production:(NSString *)goodsName {
    [_conditionLock lockWhenCondition:1]; // 只有當前鎖的條件為1時,當前線程才可以獲得該鎖來訪問資源,否則會被阻塞。
    [_goodsList addObject:goodsName];
    [_conditionLock unlockWithCondition:2]; // 解除鎖,並設置鎖的條件為2
}

/// 購買商品
- (void)buy {
    
    NSLog(@"%@ 排隊中......", [NSThread currentThread].name);
//    [_conditionLock lockWhenCondition:2]; // 只有當前鎖的條件為2時,當前線程才可以獲得該鎖來訪問資源,否則會被阻塞。
    // 我們還可以通過下面方法,來指定一個等待時間,當時間到了,如果當前線程依然沒有獲得鎖,該線程會自動被喚起
    [_conditionLock lockWhenCondition:2 beforeDate:[NSDate dateWithTimeIntervalSinceNow:6]];
    
    if (_goodsList.count == 0) {
        NSLog(@"%@ 沒有買到商品,回家了......", [NSThread currentThread].name);
    }else {
        
        NSString *goods = [_goodsList lastObject];
        [_goodsList removeLastObject];
        NSLog(@"%@ 買到了 %@", [NSThread currentThread].name, goods);
    }
    [_conditionLock unlockWithCondition:1]; // 解鎖,並設置鎖的條件為1,目的是將獲取鎖的權限交給了生產商品的線程
}

 

我們在初始化條件鎖的時候,設置了一個條件值為1

if (!_conditionLock) {
    _conditionLock = [[NSConditionLock alloc] initWithCondition:1];
}

 從這個例子可知,我們可以通過設置一個條件值,來控制哪個線程可以獲得鎖,從而獲取資源。這樣一來,我們就可以通過這個條件值來保持多個線程的執行順序了。

下面是該條件鎖的api方法,如下:

- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
- (void)lock;
- (void)unlock;

我們需要注意的是,lock與unlock只是對當前線程進行的加鎖與解鎖,並不受條件值的影響。因此我們在使用lockWhenCondition加鎖的時候,如果調用unlock解鎖,那么只會解鎖當前線程,並不會改變條件值,此時你需要再次調用unlockWithCondition來改變條件值,在實際使用中我想lockWhenCondition與unlockWithCondition應該是成對使用的,除非你有特殊的需求。

 

@synchronized(遞歸鎖)

這個鎖我們在開發中也經常看到,也實際使用過,實際上它只是一種語法糖,底層依然是我們上面用到的鎖。

我們知道 @synchronized 后面需要緊跟一個 OC 對象,它實際上是把這個對象當做鎖來使用。這是通過一個哈希表來實現的,OC 在底層使用了一個遞歸鎖的數組(你可以理解為鎖池),通過對對象取哈希值來得到對應的遞歸鎖。(注意:有些文章中提到的這里是一個互斥鎖,實際上是錯誤的,它是采用pthread_mutex實現的遞歸鎖,為啥是遞歸鎖,我們在實際使用中也可以發現,@synchronized是可以嵌套的,嵌套中的OC對象可以是相同的,既然是相同的,那么其對應的鎖肯定也是一樣的,我們知道,同一個鎖不能被同一個線程捕獲,而只有遞歸鎖允許

其內部實現實則調用如下兩個函數:

// 加鎖
func objc_sync_enter(_ obj: Any) -> Int32

// 解鎖
func objc_sync_exit(_ obj: Any) -> Int32

 Atomic(原子操作)

狹義上的原子操作表示一條不可打斷的操作,也就是說線程在執行操作過程中,不會被操作系統掛起,而是一定會執行完。在單處理器環境下,一條匯編指令顯然是原子操作,因為中斷也要通過指令來實現。

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

我們在iOS開發中經常使用的@property (atomic, assign)聲明屬性,其中atomic就是指定當前屬性為原子操作。

還有#import <libkern/OSAtomic.h>這個庫中的函數,也是原子操作的,如下:

int32_t res = OSAtomicIncrement32(&_count); // 對count進行原子+1的操作,返回操作后的結果

不過這個在iOS10之后被廢棄了,改用#import<stdatomic.h>下的如下調用(具體可以看這里):

@property (nonatomic, assign) _Atomic(int) count;
int32_t res = atomic_fetch_add(&_count, 2); // 對count進行原子+2的操作,返回操作前的結果

 

pthread_rwlock_t(讀寫鎖)

與互斥量類似,但讀寫鎖允許更高的並行性。其特性為:寫獨占,讀共享。讀寫鎖存在如下三種狀態:
  1. 讀模式下加鎖狀態(讀鎖)
  2. 寫模式下加鎖狀態(寫鎖)
  3. 不加鎖狀態
當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當它以寫模式鎖住時,它是以獨占模式鎖住的。寫獨占、讀共享。
#import <pthread.h>
@interface DrLock : NSObject {
    
    pthread_rwlock_t _lock;
}

- (void)rdLock;
- (int)tryRdLock;
- (void)wrLock;
- (int)tryWrLock;
- (void)unlock;

@end
@implementation DrLock

- (void)dealloc{
    pthread_rwlock_destroy(&_lock);
}

- (instancetype)init{
    self = [super init];
    if (self) {
        
//        pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER; // 靜態初始化方法
//        _lock = lock;
        
        pthread_rwlockattr_t attr;
        pthread_rwlockattr_init(&attr);
        pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE); // 這里設置為僅適用於當前進程所創建的線程
        pthread_rwlock_init(&_lock, &attr);
        pthread_rwlockattr_destroy(&attr);
        
    }
    return self;
}

/// 讀模式加鎖,當前線程無法獲取鎖時,將會阻塞當前線程
- (void)rdLock{
    pthread_rwlock_rdlock(&_lock);
}

/// 讀模式鎖,當前線程無法獲得鎖時,不會阻塞當前線程,直接返回EBUSY
- (int)tryRdLock{ return pthread_rwlock_tryrdlock(&_lock); } /// 寫模式加鎖,當前線程無法獲取鎖時,將會阻塞當前線程 - (void)wrLock{ pthread_rwlock_wrlock(&_lock); }
/// 寫模式鎖,當前線程無法獲得鎖時,不會阻塞當前線程,直接返回EBUSY
- (int)tryWrLock{ return pthread_rwlock_trywrlock(&_lock); } /// 解鎖 - (void)unlock{ pthread_rwlock_unlock(&_lock); } @end

當一個線程擁有了讀鎖時,執行讀操作,此時仍然可以有多個線程擁有讀鎖。但是此時一個線程嘗試擁有寫鎖時,這個線程將被阻塞,直到讀鎖被解鎖為止。

當一個線程擁有寫鎖時,執行寫操作,此時其他線程將不能獲得寫鎖,並且其他線程也不能擁有讀鎖。

這就是寫獨占、讀共享。

從上面的api中我們可以看到讀鎖和寫鎖擁有兩種方法,一個是帶try的,一個是不帶try的。帶try的方法在不能獲得鎖的情況下,不會阻塞當前線程,而是立刻返回一個EBUSY錯誤。而不帶try的方法,在當前線程無法獲得鎖時,將會阻塞當前線程,直到其他線程解了鎖。

讀寫鎖何時會產生死鎖?
即:當一個線程獲得了寫鎖后,這個線程停止工作了,那么這個寫鎖永遠不會被解鎖了,由於寫是獨占的,因此就產生了死鎖。
而一個線程獲得了讀鎖后,停止工作了,這個讀鎖會被自動解鎖,不會產生死鎖。
 
 

dispatch_barrier_async(GCD柵欄實現讀寫鎖)

GCD柵欄分為異步和同步,dispatch_barrier_async(異步柵欄),dispatch_barrier_sync(同步柵欄)。同步柵欄會阻塞在同一線程下的后面的代碼執行,我們可以根據實際情況選擇采用同步還是異步。
一般柵欄操作都執行在一個GCD的並行隊列中,如果是一個串行隊列,那么這個柵欄也就發揮不到其作用了。
柵欄的作用就是將柵欄操作的前后隊列任務分離,僅當柵欄任務完成后,其后面的隊列任務才會執行。
如下代碼事例:
    // 並行隊列
    dispatch_queue_t queue = dispatch_queue_create("CONCURRENT", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        NSLog(@"執行任務1");
        sleep(1);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"執行任務2");
        sleep(1);
    });
    
    dispatch_barrier_async(queue, ^{ // 一個異步柵欄,它不會阻塞當前線程
        NSLog(@"執行柵欄操作1");
        sleep(1);
    });
    
    dispatch_barrier_sync(queue, ^{ // 一個同步柵欄,它會阻塞當前線程
        NSLog(@"執行柵欄操作2");
        sleep(1);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"執行任務3");
        sleep(1);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"執行任務4");
        sleep(1);
    });
    
    NSLog(@"代碼執行完畢");

輸入結果如下:

執行任務1
執行任務2
執行柵欄操作1
執行柵欄操作2
代碼執行完畢 // 這里被同步柵欄阻塞了,因此在同步柵欄之后執行的
執行任務3
執行任務4

經過上面的事例,我們已經對柵欄有了了解,接下來我們舉個實際應用:

@interface MessageManager : NSObject{
    
    NSMutableArray *_msgList;
    dispatch_queue_t _queue;
}

- (NSString *)getMessage;
- (void)setMessage:(NSString *)msg;

@end
@implementation MessageManager

- (instancetype)init {
    self = [super init];
    if (self) {
        _msgList = [@[] mutableCopy];
        _queue = dispatch_queue_create("lock", DISPATCH_QUEUE_CONCURRENT); // 為了提高讀的效率,這里采用並行隊列
    }
    return self;
}

- (NSString *)getMessage{
    __block NSString *msg = nil;
    dispatch_sync(_queue, ^{ // 采用同步讀操作
        msg = [self -> _msgList componentsJoinedByString:@", "];
    });
    return msg;
}
- (void)setMessage:(NSString *)msg{
    dispatch_barrier_async(_queue, ^{ // 采用異步柵欄,執行寫操作
        [self -> _msgList addObject:msg];
    });
}

@end

上面是一個利用GCD柵欄實現的一個讀寫消息的操作

  1. 為了提高讀的效率,我們采用並行隊列實現。
  2. 讀消息是立即返回的,因此采用隊列的同步操作。
  3. 寫消息可能會花費一定時間,因此我們采用異步柵欄,這樣不會阻塞當前線程

那么除了GCD的柵欄可以實現讀寫鎖,可不可以采用GCD的group實現呢?答案是不可以。我們要清楚一點,讀寫鎖的特點是什么?寫獨占,讀共享,也就是多讀少寫。明白了這一點,我們就會知道為什么不能用group實現了。group是采用dispatch_group_waitdispatch_group_leave來實現同步操作的,這也就導致不能實現寫獨占,讀共享了。

 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM