IOS多線程 之 線程安全,線程同步,讀寫安全詳解

什么是線程不安全和線程安全?

  • 線程不安全:
    是指不提供加鎖機(jī)制保護(hù),有可能出現(xiàn)多個(gè)線程先后更改數(shù)據(jù)造成所得到的數(shù)據(jù)是臟數(shù)據(jù)。如下圖:


    image.png
  • 線程安全:
    指多個(gè)線程在執(zhí)行同一段代碼的時(shí)候采用加鎖機(jī)制,使每次的執(zhí)行結(jié)果和單線程執(zhí)行的結(jié)果都是一樣的,不存在執(zhí)行程序時(shí)出現(xiàn)意外結(jié)果。如下圖:


    image.png

如何解決線程不安全?

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

舉例:

假設(shè)售票系統(tǒng)有15張票,A,B,C同時(shí)來買票,如果是線程不安全,那么可能售票系統(tǒng)可能出現(xiàn)15-1去同時(shí)執(zhí)行的情況,最終結(jié)果是A,B,C都買完后剩下13張票,而不是12張。

/**
 賣1張票
 */
- (void)saleTicket
{
    int oldTicketsCount = self.ticketsCount;
    sleep(.2);
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    
    NSLog(@"還剩%d張票 - %@", oldTicketsCount, [NSThread currentThread]);
}

/**
 賣票演示
 */
- (void)ticketTest
{
    self.ticketsCount = 15;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        [self saleTicket];
    });
    
    dispatch_async(queue, ^{
        [self saleTicket];
    });
    
    dispatch_async(queue, ^{
        [self saleTicket];
    });
}

打印結(jié)果為:

2020-03-26 15:29:49.638680+0800 Interview03-安全隱患[12657:281738] 還剩14張票 - <NSThread: 0x600001cd03c0>{number = 9, name = (null)}
2020-03-26 15:29:49.638694+0800 Interview03-安全隱患[12657:281709] 還剩14張票 - <NSThread: 0x600001c9db00>{number = 7, name = (null)}
2020-03-26 15:29:49.638800+0800 Interview03-安全隱患[12657:281458] 還剩13張票 - <NSThread: 0x600001cbaf40>{number = 3, name = (null)}

要解決以上線程不安全的問題,就需要給每個(gè)線程加同一把鎖。

鎖又分為自旋鎖、互斥鎖、遞歸鎖讀寫鎖
  • 自旋鎖:
    自旋鎖是一種特殊的互斥鎖,當(dāng)資源被加鎖后,其它線程想要再次加鎖,此時(shí)該線程不會(huì)被阻塞睡眠而是陷入循環(huán)等待狀態(tài)(不能再做其它事情),循環(huán)檢查資源持有者是否已經(jīng)釋放了資源,這樣做的好處是減少了線程從睡眠到喚醒的資源消耗,但會(huì)一直占用CPU資源。適用于資源的鎖被持有的時(shí)間短,而不希望在線程的喚醒上花費(fèi)太多資源的情況。

  • 互斥鎖:
    一個(gè)線程獲得資源的使用權(quán)后就會(huì)將改資源加鎖,使用完后會(huì)將其解鎖,所以在使用過程中有其它線程想要獲取該資源的鎖,那么它就會(huì)被阻塞陷入睡眠狀態(tài),直到該資源被解鎖才會(huì)別喚醒,如果被阻塞的資源不止一個(gè),那么它們都會(huì)被喚醒,但是獲得資源使用權(quán)的是第一個(gè)被喚醒的線程,其它線程又陷入沉睡。

  • 遞歸鎖:
    同一個(gè)線程可以多次獲得該資源鎖,別的線程必須等待該線程釋放所有次數(shù)的鎖才能獲得。

  • 讀寫鎖:
    讀寫鎖擁有讀狀態(tài)加鎖、寫狀態(tài)加鎖、不加鎖三種狀態(tài)。只有一個(gè)線程可以占有寫狀態(tài)的鎖,但可以多個(gè)線程同時(shí)占有讀狀態(tài)鎖,這也是它可以實(shí)現(xiàn)高并發(fā)的原因。當(dāng)其處于寫狀態(tài)鎖下,任何想要嘗試獲得鎖的線程都會(huì)被阻塞,直到寫狀態(tài)鎖被釋放;如果是處于讀狀態(tài)鎖下,允許其它線程獲得它的讀狀態(tài)鎖,但是不允許獲得它的寫狀態(tài)鎖,當(dāng)讀寫鎖感知到有線程想要獲得寫狀態(tài)鎖時(shí),便會(huì)阻塞其后所有想要獲得讀狀態(tài)鎖的線程。所以讀寫鎖非常適合資源的讀操作遠(yuǎn)多于寫操作的情況。

有以下10種加鎖方案:

注意:在不同的線程中加鎖,必須是同一把鎖。

  1. OSSpinLock:需導(dǎo)入頭文件#import <libkern/OSAtomic.h>,是自旋鎖。
    目前已經(jīng)不再安全,可能會(huì)出現(xiàn)優(yōu)先級反轉(zhuǎn)問題。(如果等待鎖的線程的優(yōu)先級較高,它會(huì)一直占用CPU資源,優(yōu)先級低的線程就無法釋放鎖)

    //初始化鎖
    OSSpinLock lock = OS_SPINLOCK_INIT;
    //加鎖
    OSSpinLockLock(&lock);
    //解鎖
    OSSpinLockUnlock(&lock);
    
  2. os_unfair_lock:需導(dǎo)入頭文件#import <os/lock.h>,是互斥鎖
    用于取代不安全的OSSpinLock,從IOS10開始才支持,從底層調(diào)用看,等待os_unfair_lock鎖的線程會(huì)處于休眠狀態(tài),并非忙等。

    //初始化鎖
    os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
    //加鎖
    os_unfair_lock_lock(&lock);
    //解鎖
    os_unfair_lock_unlock(&lock);
    
  3. pthread_mutex_t:需導(dǎo)入頭文件#import <pthread.h>,是互斥鎖。

    //申明mutex鎖
    pthread_mutex_t mutex;
    //聲明mutex鎖的屬性
    pthread_mutexattr_t attr;
    //初始化屬性attr
    pthread_mutexattr_init(&attr);
    //設(shè)置鎖屬性的類型
    /* 參數(shù)說明
      *
      * 第一個(gè)參數(shù)pthread_mutexattr_t:mutex鎖屬性attr對象的地址
      * 第二個(gè)參數(shù):鎖的類型
                  #define PTHREAD_MUTEX_NORMAL          0 //默認(rèn)鎖,也就是”互斥鎖“
                  #define PTHREAD_MUTEX_ERRORCHECK      1 //錯(cuò)誤鎖
                  #define PTHREAD_MUTEX_RECURSIVE       2 //遞歸鎖
                  #define PTHREAD_MUTEX_DEFAULT         PTHREAD_MUTEX_NORMAL
     */
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    //初始化鎖
    /* 參數(shù)說明
      *
      * 第一個(gè)參數(shù)pthread_mutex_t:mutex鎖對象的地址
      * 第二個(gè)參數(shù)pthread_mutexattr_t:mutex鎖屬性attr對象的地址。該參數(shù)可以傳NULL,為默認(rèn)屬性PTHREAD_MUTEX_DEFAULT,”互斥鎖“
     */
    pthread_mutex_init(&mutex, &attr);
    //銷毀mutex鎖的屬性
    pthread_mutexattr_destroy(&attr);
    //加鎖
    pthread_mutex_lock(&mutex);
    //解鎖
    pthread_mutex_unlock(&mutex);
    //銷毀鎖
    pthread_mutexattr_destroy(&mutex);
    

    擴(kuò)展:
    pthread_cond_t條件鎖:配合pthread_mutex_t實(shí)現(xiàn)

    //初始化條件鎖
    pthread_cond_t cond;
    pthread_cond_init(&cond, NULL);
    //條件等待,當(dāng)線程調(diào)用該函數(shù),然后阻塞當(dāng)前線程進(jìn)入休眠并解鎖。
    pthread_cond_wait(&cond, &mutex);
    //條件喚醒,當(dāng)線程中調(diào)用該函數(shù),等到該調(diào)用線程解鎖后,然后h就喚醒前面休眠的線程,并給喚醒線程加鎖。
    //喚醒一個(gè)等待該條件的線程
    pthread_cond_signal(&cond);
    //喚醒所有等待該條件的線程
    pthread_cond_broadcast(&cond);
    //銷毀條件
    pthread_cond_destroy(&cond);
    
  4. NSLock:對pthread_mutex_t的鎖封裝。

    //初始化鎖
    NSLock *lock = [[NSLock alloc] init];
    //加鎖
    [lock lock];
    //解鎖
    [lock unlock];
    
  5. NSRecursiveLock::對pthread_mutex_t的的遞歸鎖封裝。

    //初始化鎖
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
    //加鎖
    [recursiveLock lock];
    //解鎖
    [recursiveLock unlock];
    
  6. NSCondition:對pthread_mutex_t的cond條件和mutex鎖封裝。

    //初始化鎖
    NSCondition *cond = [[NSCondition alloc] init];
    //加鎖
    [cond lock];
    //條件等待
    [cond wait];
    //喚醒條件等待
    //喚醒一個(gè)條件等待
    [cond signal];
    //喚醒所有條件等待
    [cond broadcast];
    //解鎖
    [cond unlock];
    
  7. NSConditionLock:對pthread_mutex_的cond條件和mutex鎖封裝。會(huì)根據(jù)內(nèi)部存儲(chǔ)的條件condition的值,進(jìn)行加鎖。

    //初始化鎖
    NSConditionLock *condLock = [[NSConditionLock alloc] initWithCondition:1];
    //加鎖
    [condLock lockWhenCondition:1];
    //解鎖
    [condLock unlockWithCondition:2];
    //加鎖
    [condLock lockWhenCondition:2];
    //解鎖
    [condLock unlock];
    
  8. 在同步串行隊(duì)列中開啟子線程執(zhí)行任務(wù)。

    dispatch_sync(dispatch_queue_create("sertal", DISPATCH_QUEUE_SERIAL), ^{
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            //執(zhí)行操作
        });
    });
    
  9. 使用信號量dispatch_semaphore_t控制線程最大并發(fā)數(shù)量:

    // 信號量的初始值
    int value = 1;
    //初始化信號量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
    // 如果信號量的值<=0,當(dāng)前線程就進(jìn)入休眠等待(直到信號量的值>0)。
    // 如果信號量的值>0,就減1,然后往下執(zhí)行后面的代碼。
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    // 讓信號量的值加1
    dispatch_semaphore_signal(semaphore);
    
  10. @synchronized:是封裝了mutex的遞歸鎖。

    @synchronized([self class]) {
        //執(zhí)行操作
    }
    

IOS線程同步方案性能比較:

從高到低:
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized

多線程文件讀寫安全,多讀單寫:

  • 使用讀寫鎖pthread_rwlock_t:是“互斥鎖”。

    //聲明讀寫鎖
    pthread_rwlock_t rwlock;
    //初始化讀寫鎖
    pthread_rwlock_init(&rwlock, NULL);
    //讀的加鎖
    pthread_rwlock_rdlock(&rwlock);
    //讀的解鎖
    pthread_rwlock_unlock(&rwlock);
    
    //寫的加鎖
    pthread_rwlock_wrlock(&rwlock);
    //寫的解鎖
    pthread_rwlock_unlock(&rwlock);
    
    //銷毀鎖
    pthread_rwlock_destroy(&rwlock);
    
  • 異步函數(shù)dispatch_async進(jìn)行讀的操作和異步柵欄dispatch_barrier_async進(jìn)行寫的操作:讀寫操作必須保證在同一個(gè)手動(dòng)創(chuàng)建的并發(fā)隊(duì)列中。

    //必須手動(dòng)創(chuàng)建并發(fā)隊(duì)列
    dispatch_queue_t queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
    //異步h函數(shù)執(zhí)行讀的操作
    dispatch_async(queue, ^{
        //讀的操作
    });
    
    //異步柵欄函數(shù)進(jìn)行寫的操作,隊(duì)列必須是手動(dòng)創(chuàng)建的全局并發(fā)隊(duì)列。
    //如果不是,傳入的是串行隊(duì)列或者全局并發(fā)隊(duì)列,異步柵欄會(huì)失效,效果等同于異步函數(shù)dispatch_async
    dispatch_barrier_async(queue, ^{
        //寫的操作
    });
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

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