iOS 底層 - 多線程安全隱患之加鎖

本文源自本人的學(xué)習(xí)記錄整理與理解,其中參考閱讀了部分優(yōu)秀的博客和書籍,盡量以通俗簡(jiǎn)單的語句轉(zhuǎn)述。引用到的地方如有遺漏或未能一一列舉原文出處還望見諒與指出,另文章內(nèi)容如有不妥之處還望指教,萬分感謝 !

多線程安全隱患表現(xiàn)在那些方面 ?

資源共享

  • 一塊資源可能會(huì)被多個(gè)線程共享,也就是多個(gè)線程可能會(huì)訪問同一塊資源
  • 比如:多個(gè)線程訪問同一個(gè)對(duì)象、同一個(gè)變量、同一個(gè)文件

當(dāng)多個(gè)線程訪問同一塊資源時(shí),很容易引發(fā)數(shù)據(jù)錯(cuò)亂和數(shù)據(jù)安全問題;

多線程數(shù)據(jù)錯(cuò)亂: 顧名思義就是數(shù)據(jù)出現(xiàn)混亂,比如:3條線程在給變量age賦值同時(shí)又有9條線程來取這個(gè)age的值,那么取出來的值就可能會(huì)各不相同;這樣就相當(dāng)于是數(shù)據(jù)出現(xiàn)了錯(cuò)亂 !

多線程數(shù)據(jù)安全:多線程訪問導(dǎo)致了數(shù)據(jù)出現(xiàn)錯(cuò)亂,從而就可能引發(fā)數(shù)據(jù)的安全問題。比如:存取錢時(shí)多條線程同時(shí)操作可能就會(huì)造成錢越取越多或越存越少。

圖解

多線程安全隱患分析@2x.png

解決辦法:

  • 采用線程同步技術(shù)(同步:協(xié)同步調(diào),按預(yù)定的先后次序進(jìn)行)
  • 常見的線程同步技術(shù)是:加鎖
加鎖@2x.png

性能從高到低排序
os_unfair_lock 互斥鎖
OSSpinLock 自旋鎖
dispatch_semaphore 信號(hào)量
pthread_mutex 互斥鎖
dispatch_queue(DISPATCH_QUEUE_SERIAL) 串行隊(duì)列
NSLock 普通(互斥)鎖
NSCondition 條件鎖
pthread_mutex(recursive) 遞歸鎖
NSRecursiveLock 遞歸鎖
NSConditionLock 條件鎖
@synchronized 遞歸鎖

推薦使用dispatch_semaphore (ios 4開始)、pthread_mutex、
os_unfair_lock 從 IOS10開始,所以如果老版本不建議使用

OSSpinLock 自旋鎖

使用介紹:

  • 需要導(dǎo)入頭文件#import <libkern/OSAtomic.h>
WX20200408-213529@2x.png

注意: OSSpinLock初始化賦值需要靜態(tài)初始化,應(yīng)該直接賦值;如果是直接一個(gè)方法調(diào)用,用返回值賦值給他是會(huì)報(bào)錯(cuò)。

  • 等待鎖的線程會(huì)處于忙等(busy-wait)狀態(tài),一直占用CPU資源
  • 屬于High-level lock (高級(jí)鎖),特點(diǎn)就是等不到鎖就一直在循環(huán)等待
  • 目前已經(jīng)不在安全,可能會(huì)出現(xiàn)優(yōu)先級(jí)反轉(zhuǎn)問題;所以從IOS10開始不推薦使用 !,建議使用os_unfair_lock
    • 如果等待鎖的線程優(yōu)先級(jí)較高,它會(huì)一直占用這CPU資源,優(yōu)先級(jí)低的線程就無法釋放鎖 ;

比如:
開啟了thread1(優(yōu)先級(jí)最高)thread2(優(yōu)先級(jí)最低)這兩條線程來執(zhí)行相同任務(wù),如果thread2先進(jìn)來執(zhí)行,就會(huì)先加鎖準(zhǔn)備執(zhí)行任務(wù);
這時(shí)候thread1剛好進(jìn)來了,發(fā)現(xiàn)線程已經(jīng)被加過鎖了那它只能忙等;忙等相當(dāng)于是在while循環(huán)等待,這也是需要消耗CPU給分配的資源的,由于thread1優(yōu)先級(jí)最高肯定會(huì)分配到更多的資源,這樣可能會(huì)造成thread2沒有資源可被利用無法繼續(xù)執(zhí)行自己的代碼,沒發(fā)繼續(xù)執(zhí)行也就沒辦法解鎖了,thread2家的這把鎖就無法釋放了!

資源搶奪結(jié)果:thread2的無法釋放,thread1一直在忙等;最終就造成死鎖咯 !

解決這種情況需要把忙等改為休眠,就是等待的這個(gè)線程讓他休眠;而這種技術(shù)在os_unfair_lock中實(shí)現(xiàn)了!

os_unfair_lock 互斥鎖的一種

  • os_unfair_lock用于取代不安全的OSSpinLock,從IOS10開始才支持
  • 從底層調(diào)用看,等待os_unfair_lock鎖的線程會(huì)處于休眠狀態(tài),并非忙等
  • 屬于low-level lock(低級(jí)鎖) ,特點(diǎn)等不到鎖就休眠

使用介紹:

  • 需要導(dǎo)入頭文件#imorpt <os/lock.h>
WX20200408-213908@2x.png

dispatch_semaphore 信號(hào)量

  • 信號(hào)量的初始值,可以用來控制線程的并發(fā)訪問的最大數(shù)量;NSOperationQueue的maxConcurrentOperationCount也可以做到
  • 信號(hào)量的初始值為1,代表同時(shí)只允許1條線程訪問資源,保證線程同步
  • 等待時(shí)進(jìn)行休眠
WX20200408-214405@2x.png

示例代碼

  self.semaphore = dispatch_semaphore_create(5);

- (void)otherTest
{
//開啟20條子線程都來執(zhí)行test方法
    for (int i = 0; i < 20; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
    }
}

// 線程10、7、6、9、8
- (void)test
{
    // 如果信號(hào)量的值 > 0,就讓信號(hào)量的值減1,然后繼續(xù)往下執(zhí)行代碼
    // 如果信號(hào)量的值 <= 0,就會(huì)休眠等待,直到信號(hào)量的值變成>0,就讓信號(hào)量的值減1,然后繼續(xù)往下執(zhí)行代碼
    //#define DISPATCH_TIME_NOW (0ull)       不需要等
    //#define DISPATCH_TIME_FOREVER (~0ull)  一直等
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(2);
    NSLog(@"test - %@", [NSThread currentThread]);
    
    // 讓信號(hào)量的值+1
    dispatch_semaphore_signal(self.semaphore);
    
    //減一再加一剛好保持不變;
    
}

如果每個(gè)線程都需要不同的鎖可以用宏定義的方式

#define SemaphoreBegin \
static dispatch_semaphore_t semaphore; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
    semaphore = dispatch_semaphore_create(1); \
}); \
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);

pthread_mutex

  • pthread開頭的都是跨平臺(tái)在Linux、unix、Windows、IOS都可以使用
  • mutex 叫做“互斥鎖”,等待鎖的線程會(huì)處于休眠狀態(tài)
  • 此類鎖不用是需要手動(dòng)銷毀的
pthread_mutex@2x.png

使用介紹:

  • 鎖的屬性值
 #define  PTHREAD_MUTEX_NORMAL  0             普通互斥鎖
 #define  PTHREAD_MUTEX_DEFAULT  0             普通互斥鎖
 #define  PTHREAD_MUTEX_ERRORCHECK  1    檢查錯(cuò)誤互斥鎖
 #define  PTHREAD_MUTEX_RECURSIVE       2    遞歸鎖(遞歸互斥鎖)
  • 需要導(dǎo)入頭文件#import <pthread.h>

普通鎖 PTHREAD_MUTEX_DEFAULT

- (void)__initMutex:(pthread_mutex_t *)mutex
{
    // 靜態(tài)初始化
    //pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    
    // 動(dòng)態(tài)初始化鎖
    pthread_mutex_init(mutex, NULL); //PTHREAD_MUTEX_DEFAULT

}

- (void)__saveMoney
{
   //加鎖
    pthread_mutex_lock(&_moneyMutex);
    [super __saveMoney];
    //解鎖
    pthread_mutex_unlock(&_moneyMutex);
}

- (void)dealloc
{
   //銷毀鎖
    pthread_mutex_destroy(&_moneyMutex);
}

遞歸鎖 pthread_mutex(recursive) PTHREAD_MUTEX_RECURSIVE

遞歸鎖@2x.png

注意:遞歸鎖允許同一個(gè)線程對(duì)一把鎖進(jìn)行重復(fù)加鎖

條件鎖

條件鎖@2x.png
  • 示例代碼
- (instancetype)init
{
    if (self = [super init]) {
        // 初始化屬性
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        // 初始化鎖
        pthread_mutex_init(&_mutex, &attr);
        // 銷毀屬性
        pthread_mutexattr_destroy(&attr);
        
        // 初始化條件
        pthread_cond_init(&_cond, NULL);
        
        self.data = [NSMutableArray array];
    }
    return self;
}

- (void)otherTest
{
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 生產(chǎn)者-消費(fèi)者模式
生產(chǎn)者負(fù)責(zé)生產(chǎn)商品
消費(fèi)者負(fù)責(zé)購(gòu)買
生產(chǎn)者生產(chǎn)出來商品就需要通知消費(fèi)者可以購(gòu)買商品了,在此之前消費(fèi)者可以在休眠等待
// 線程1 消費(fèi)者
// 刪除數(shù)組中的元素
- (void)__remove
{
    pthread_mutex_lock(&_mutex);
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
        // 等待就開始休眠,同時(shí)放開鎖;_cond該參數(shù)是用于將來喚醒的
        pthread_cond_wait(&_cond, &_mutex);
    }
    
    [self.data removeLastObject];
    NSLog(@"刪除了元素");
    
    pthread_mutex_unlock(&_mutex);
}

// 線程2 生產(chǎn)者
// 往數(shù)組中添加元素
- (void)__add
{
    pthread_mutex_lock(&_mutex);
    
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    
    // 信號(hào)
    pthread_cond_signal(&_cond);
    // 廣播
//    pthread_cond_broadcast(&_cond);
    
    pthread_mutex_unlock(&_mutex);
}

- (void)dealloc
{
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
}

dispatch_queue(DISPATCH_QUEUE_SERIAL)

  • 直接使用GCD的串行隊(duì)列,也是可以實(shí)現(xiàn)線程同步的
串行隊(duì)列.png

NSLock、 NSRecursoveLock

  • NSLock是對(duì)mutex普通鎖的一個(gè)OC版本封裝,說白了就是普通互斥鎖!
  • NSRecursiveLock也是對(duì)mutex recursove(遞歸鎖)的OC版本封裝,API跟NSLock基本一致

    NSLock、 NSRecursoveLock@2x.png

常用方法解讀

/**
嘗試加鎖,如果加上鎖就返回YES,不會(huì)阻塞線程;
*/
- (BOOL)tryLock;

/**
傳入一個(gè)時(shí)間參數(shù)limit,在這個(gè)時(shí)間之前我能夠等到這把鎖放開的話,我就加鎖成功返回YES;
在此之前等不到我就阻塞線程、睡覺;如果時(shí)間到了還是沒有等到鎖被放開,那就加鎖失敗返回NO!
有返回結(jié)果后,代碼就會(huì)往下執(zhí)行
*/
- (BOOL)lockBeforeDate:(NSDate *)limit;

/**
加鎖
*/
- (void)lock;

/**
解鎖
*/
- (void)unlock;

NSCondition

  • NSCondition是對(duì)mutexcond的OC版本封裝,說白了就是條件鎖
NSCondition.png

常用方法解讀

//等待
- (void)wait;
//在某個(gè)時(shí)間點(diǎn)之前等待,時(shí)間過了自己就會(huì)結(jié)束休眠
- (BOOL)waitUntilDate:(NSDate *)limit;
//發(fā)部信號(hào)
- (void)signal;
//發(fā)廣播
- (void)broadcast;
  • 示例代碼
- (instancetype)init
{
    if (self = [super init]) {
        self.condition = [[NSCondition alloc] init];
        self.data = [NSMutableArray array];
    }
    return self;
}

- (void)otherTest
{
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 生產(chǎn)者-消費(fèi)者模式

// 線程1
// 刪除數(shù)組中的元素
- (void)__remove
{
    [self.condition lock];
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
        // 等待
        [self.condition wait];
    }
    
    [self.data removeLastObject];
    NSLog(@"刪除了元素");
    
    [self.condition unlock];
}

// 線程2
// 往數(shù)組中添加元素
- (void)__add
{
    [self.condition lock];
    
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");

    // 信號(hào)
    [self.condition signal];
    // 廣播
//    [self.condition broadcast];

    [self.condition unlock];
    
}

NSConditionLock

  • NSConditionLock是對(duì)NSCondition的進(jìn)一步封裝,可以設(shè)置具體的條件值;本質(zhì)上還是條件鎖
NSConditionLock.png

可以實(shí)現(xiàn)線程間的依賴,類似NSOperationQueue的依賴;

- (instancetype)init
{
    if (self = [super init]) {
        self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    }
    return self;
}

- (void)otherTest
{
    [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start];
}

//線程__three依賴__two,__two依賴__one
- (void)__one
{
    [self.conditionLock lock];
    
    NSLog(@"__one");
    sleep(1);
    
    [self.conditionLock unlockWithCondition:2];
}

- (void)__two
{
    [self.conditionLock lockWhenCondition:2];
    
    NSLog(@"__two");
    sleep(1);
    
    [self.conditionLock unlockWithCondition:3];
}

- (void)__three
{
    [self.conditionLock lockWhenCondition:3];
    
    NSLog(@"__three");
    
    [self.conditionLock unlock];
}

@synchronized

  • @synchronized是對(duì)mutex遞歸鎖(pthread_mutex(recursive))的封裝
  • 源碼查看:objc4中的objc-sync.mm文件
  • @synchronized(obj)內(nèi)部會(huì)生成obj對(duì)應(yīng)的遞歸鎖,然后進(jìn)行加鎖、解鎖操作
  • 性能差,官方不推薦使用;且編寫時(shí)沒有提示
  • 使用起來非常簡(jiǎn)單,是加鎖技術(shù)中最簡(jiǎn)單
@synchronized.png
  • 拿到需要加鎖的對(duì)象,傳入對(duì)象從散列表中取出對(duì)應(yīng)的鎖;
散列表(哈希)
static StripeMap<SyncList> sDataLists
SyncData *data = sDataLists[obj].data
data->mutex.lock()

自旋鎖、互斥鎖比較

  • 臨界區(qū):加鎖和解鎖中間的代碼區(qū)域

什么情況使用自旋鎖比較劃算?

  • 預(yù)計(jì)線程等待鎖的時(shí)間很短
  • 加鎖的代碼(臨界區(qū))經(jīng)常被調(diào)用,但競(jìng)爭(zhēng)情況很少發(fā)生(很少出現(xiàn)多條線程同時(shí)訪問的情況)
  • CPU資源不緊張
  • 多核處理器

什么情況使用互斥鎖比較劃算?

  • 預(yù)計(jì)線程等待鎖的時(shí)間較長(zhǎng)
  • 單核處理器
  • 臨界區(qū)有IO操作(文件的讀寫操作)
  • 臨界區(qū)代碼復(fù)雜或者循環(huán)量大
  • 臨界區(qū)競(jìng)爭(zhēng)非常激烈
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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