iOS多線程的鎖,你知道多少?

前言

iOS開發(fā)中由于各種第三方庫(kù)的高度封裝,對(duì)鎖的使用很少,剛好之前面試中被問到的關(guān)于并發(fā)編程鎖的問題,都是一知半解,于是決定整理一下關(guān)于iOS中鎖的知識(shí),為大家查缺補(bǔ)漏。

目錄

第一部分: 什么是鎖

第二部分: 鎖的分類

第三部分: 性能對(duì)比

第四部分: 常見的死鎖

第五部分: 總結(jié)(附Demo)

正文

一、什么是鎖

在過去幾十年并發(fā)研究領(lǐng)域的出版物中,鎖總是扮演著壞人的角色,鎖背負(fù)的指控包括引起死鎖、鎖封護(hù)(luyang注:lock convoying,多個(gè)同優(yōu)先級(jí)的線程重復(fù)競(jìng)爭(zhēng)同一把鎖,此時(shí)大量雖然被喚醒而得不到鎖的線程被迫進(jìn)行調(diào)度切換,這種頻繁的調(diào)度切換相當(dāng)影響系統(tǒng)性能)、饑餓、不公平、data races以及其他許多并發(fā)帶來的罪孽。有趣的是,在共享內(nèi)存并行軟件中真正承擔(dān)重?fù)?dān)的是——你猜對(duì)了——鎖。

在計(jì)算機(jī)科學(xué)中,鎖是一種同步機(jī)制,用于多線程環(huán)境中對(duì)資源訪問的限制。你可以理解成它用于排除并發(fā)的一種策略。

    if (lock == 0) {
        lock = myPID;
    }復(fù)制代碼

上面這段代碼并不能保證這個(gè)任務(wù)有鎖,因此它可以在同一時(shí)間被多個(gè)任務(wù)執(zhí)行。這個(gè)時(shí)候就有可能多個(gè)任務(wù)都檢測(cè)到lock是空閑的,因此兩個(gè)或者多個(gè)任務(wù)都將嘗試設(shè)置lock,而不知道其他的任務(wù)也在嘗試設(shè)置lock。這個(gè)時(shí)候就會(huì)出問題了。再看看下面這段代碼(Swift):

    class Account {
    private(set) var val: Int = 0 //這里不可在其他方法修改,只能通過add/minus修改
    public func add(x: Int) {
        objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
        }
        val += x
    }

    public func minus(x: Int) {
        objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
        }
        val -= x;
    }
}復(fù)制代碼

這樣就能防止多個(gè)任務(wù)去修改val了。

二、鎖的分類

鎖根據(jù)不同的性質(zhì)可以分成不同的類。

在WiKiPedia介紹中,一般的鎖都是建議鎖,也就四每個(gè)任務(wù)去訪問公共資源的時(shí)候,都需要取得鎖的資訊,再根據(jù)鎖資訊來確定是否可以存取。若存取對(duì)應(yīng)資訊,鎖的狀態(tài)會(huì)改變?yōu)殒i定,因此其他線程不會(huì)訪問該資源,當(dāng)結(jié)束訪問時(shí),鎖會(huì)釋放,允許其他任務(wù)訪問。有些系統(tǒng)有強(qiáng)制鎖,若未經(jīng)授權(quán)的鎖訪問鎖定的資料,在訪問時(shí)就會(huì)產(chǎn)生異常。

在iOS中,鎖分為互斥鎖、遞歸鎖、信號(hào)量、條件鎖、自旋鎖、讀寫鎖(一種特所的自旋鎖)、分布式鎖。

對(duì)于數(shù)據(jù)庫(kù)的鎖分類:

分類方式 分類
按鎖的粒度劃分 表級(jí)鎖、行級(jí)鎖、頁(yè)級(jí)鎖
按鎖的級(jí)別劃分 共享鎖、排他鎖
按加鎖的方式劃分 自動(dòng)鎖、顯示鎖
按鎖的使用方式劃分 樂觀鎖、悲觀鎖
按操作劃分 DML鎖、DDL鎖

這里就不再詳細(xì)的介紹了,感興趣的大家可以帶Wiki
查閱相關(guān)資料。

1、互斥鎖

在編程中,引入對(duì)象互斥鎖的概念,來保證共享數(shù)據(jù)操作的完整性。每個(gè)對(duì)象都對(duì)應(yīng)于一個(gè)可稱為“互斥鎖”的標(biāo)記,這個(gè)標(biāo)記用來保證在任一時(shí)刻,只能有一個(gè)線程訪問對(duì)象。

1.1 @synchronized

  • @synchronized要一個(gè)參數(shù),這個(gè)參數(shù)相當(dāng)于信號(hào)量
// 用在防止多線程訪問屬性上比較多
- (void)setTestInt:(NSInteger)testInt {
    @synchronized (self) {
        _testInt = testInt;
    }
}復(fù)制代碼

1.2 NSLock

  • block及宏定義
// 定義block類型
typedef void(^MMBlock)(void);

// 定義獲取全局隊(duì)列方法
#define MM_GLOBAL_QUEUE(block) \
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ \
    while (1) { \
        block();\
    }\
})復(fù)制代碼

  • 測(cè)試代碼
NSLock *lock = [[NSLock alloc] init];
MMBlock block = ^{
    [lock lock];
    NSLog(@"執(zhí)行操作");
    sleep(1);
    [lock unlock];
};
MM_GLOBAL_QUEUE(block);復(fù)制代碼

1.3 pthread

pthread除了創(chuàng)建互斥鎖,還可以創(chuàng)建遞歸鎖、讀寫鎖、once等鎖。稍后會(huì)介紹一下如何使用。如果想要深入學(xué)習(xí)pthread請(qǐng)查閱相關(guān)文檔、資料單獨(dú)學(xué)習(xí)。

  • 靜態(tài)初始化: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

  • 動(dòng)態(tài)初始化: pthread_mutex_init() 函數(shù)是以動(dòng)態(tài)方式創(chuàng)建互斥鎖的,參數(shù) attr 指定了新建互斥鎖的屬性。如果參數(shù) attrNULL ,使用默認(rèn)的屬性,返回0代表初始化成功。這種方式可以初始化普通鎖、遞歸鎖(同 NSRecursiveLock ), 初始化方式有些復(fù)雜。

  • 此類初始化方法可設(shè)置鎖的類型,PTHREAD_MUTEX_ERRORCHECK 互斥鎖不會(huì)檢測(cè)死鎖, PTHREAD_MUTEX_ERRORCHECK 互斥鎖可提供錯(cuò)誤檢查, PTHREAD_MUTEX_RECURSIVE 遞歸鎖, PTHREAD_PROCESS_DEFAULT 映射到 PTHREAD_PROCESS_NORMAL.

  • 下面是我從YYKitcopy下來的:

#import <pthread.h>

//YYKit
static inline void pthread_mutex_init_recursive(pthread_mutex_t *mutex, bool recursive) {
#define YYMUTEX_ASSERT_ON_ERROR(x_) do { \
__unused volatile int res = (x_); \
assert(res == 0); \
} while (0)
    assert(mutex != NULL);
    if (!recursive) {
        //普通鎖
        YYMUTEX_ASSERT_ON_ERROR(pthread_mutex_init(mutex, NULL));
    } else {
        //遞歸鎖
        pthread_mutexattr_t attr;
        YYMUTEX_ASSERT_ON_ERROR(pthread_mutexattr_init (&attr));
        YYMUTEX_ASSERT_ON_ERROR(pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE));
        YYMUTEX_ASSERT_ON_ERROR(pthread_mutex_init (mutex, &attr));
        YYMUTEX_ASSERT_ON_ERROR(pthread_mutexattr_destroy (&attr));
    }
#undef YYMUTEX_ASSERT_ON_ERROR
}復(fù)制代碼

  • 測(cè)試代碼
__block pthread_mutex_t lock;
    pthread_mutex_init_recursive(&lock,false);

    MMBlock block0=^{
        NSLog(@"線程 0:加鎖");
        pthread_mutex_lock(&lock);
        NSLog(@"線程 0:睡眠 1 秒");
        sleep(1);
        pthread_mutex_unlock(&lock);
        NSLog(@"線程 0:解鎖");
    };
    MM_GLOBAL_QUEUE(block0);

    MMBlock block1=^(){
        NSLog(@"線程 1:加鎖");
        pthread_mutex_lock(&lock);
        NSLog(@"線程 1:睡眠 2 秒");
        sleep(2);
        pthread_mutex_unlock(&lock);
        NSLog(@"線程 1:解鎖");
    };
    MM_GLOBAL_QUEUE(block1);

    MMBlock block2=^{
        NSLog(@"線程 2:加鎖");
        pthread_mutex_lock(&lock);
        NSLog(@"線程 2:睡眠 3 秒");
        sleep(3);
        pthread_mutex_unlock(&lock);
        NSLog(@"線程 2:解鎖");
    };
    MM_GLOBAL_QUEUE(block2);復(fù)制代碼

  • 輸出結(jié)果:
 線程 2:加鎖
 線程 0:加鎖
 線程 1:加鎖
 線程 2:睡眠 3 秒復(fù)制代碼

 線程 2:加鎖
 線程 0:加鎖
 線程 1:加鎖
 線程 2:睡眠 3 秒
 線程 2:解鎖
 線程 0:睡眠 1 秒
 線程 2:加鎖復(fù)制代碼

 線程 2:加鎖
 線程 0:加鎖
 線程 1:加鎖
 線程 2:睡眠 3 秒
 線程 2:解鎖
 線程 0:睡眠 1 秒
 線程 2:加鎖
 線程 0:解鎖
 線程 1:睡眠 2 秒
 線程 0:加鎖復(fù)制代碼

2、遞歸鎖

同一個(gè)線程可以多次加鎖,不會(huì)造成死鎖

舉個(gè)??:

NSLock *lock = [[NSLock alloc] init];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    static void (^RecursiveMethod)(int);

    RecursiveMethod = ^(int value) {

        [lock lock];
        if (value > 0) {

            NSLog(@"value = %d", value);
            sleep(2);
            RecursiveMethod(value - 1);
        }
        [lock unlock];
    };

    RecursiveMethod(5);
});復(fù)制代碼

這段代碼是一個(gè)典型的死鎖情況。在我們的線程中,RecursiveMethod是遞歸調(diào)用的。所有每次進(jìn)入這個(gè)block時(shí),都會(huì)去加一次鎖,而從第二次開始,由于鎖已經(jīng)被使用了且沒有解鎖,所有它需要等待鎖被解除,這樣就導(dǎo)致了死鎖,線程被阻塞住了??刂婆_(tái)會(huì)輸出如下信息:

value = 5
*** -[NSLock lock]: deadlock ( '(null)')   *** Break on _NSLockError() to debug.復(fù)制代碼

2.1 NSRecursiveLock

  • 實(shí)現(xiàn)代碼
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    MM_GLOBAL_QUEUE(^{
        static void (^RecursiveBlock)(int);
        RecursiveBlock = ^(int value) {
            [lock lock];
            if (value > 0) {
                NSLog(@"加鎖層數(shù) %d", value);
                sleep(1);
                RecursiveBlock(--value);
            }
            [lock unlock];
        };
        RecursiveBlock(3);
    });復(fù)制代碼

  • 輸出結(jié)果(從輸出結(jié)果可以看出并未發(fā)生死鎖):
加鎖層數(shù) 3
加鎖層數(shù) 2
加鎖層數(shù) 1
加鎖層數(shù) 3
加鎖層數(shù) 2
加鎖層數(shù) 1
加鎖層數(shù) 3
加鎖層數(shù) 2復(fù)制代碼

2.2 pthread

  • 代碼實(shí)現(xiàn)
__block pthread_mutex_t lock;
    //第二個(gè)參數(shù)為true生成遞歸鎖
    pthread_mutex_init_recursive(&lock,true);

    MM_GLOBAL_QUEUE(^{
        static void (^RecursiveBlock)(int);
        RecursiveBlock = ^(int value) {
            pthread_mutex_lock(&lock);
            if (value > 0) {
                NSLog(@"加鎖層數(shù) %d", value);
                sleep(1);
                RecursiveBlock(--value);
            }
            pthread_mutex_unlock(&lock);
        };
        RecursiveBlock(3);
    });復(fù)制代碼

  • 輸出結(jié)果(同樣,結(jié)果顯示并未發(fā)生死鎖):
加鎖層數(shù) 3
加鎖層數(shù) 2
加鎖層數(shù) 1
加鎖層數(shù) 3
加鎖層數(shù) 2
加鎖層數(shù) 1
加鎖層數(shù) 3
加鎖層數(shù) 2復(fù)制代碼

3、信號(hào)量

信號(hào)量(Semaphore),有時(shí)被稱為信號(hào)燈,是在多線程環(huán)境下使用的一種設(shè)施,是可以用來保證兩個(gè)或多個(gè)關(guān)鍵代碼段不被并發(fā)調(diào)用。在進(jìn)入一個(gè)關(guān)鍵代碼段之前,線程必須獲取一個(gè)信號(hào)量;一旦該關(guān)鍵代碼段完成了,那么該線程必須釋放信號(hào)量。其它想進(jìn)入該關(guān)鍵代碼段的線程必須等待直到第一個(gè)線程釋放信號(hào)量

3.1 dispatch_semaphore_t

  • 同步實(shí)現(xiàn)
// 參數(shù)可以理解為信號(hào)的總量,傳入的值必須大于或等于0,否則,返回NULL
// dispatch_semaphore_signal + 1
// dispatch_semaphore_wait等待信號(hào),當(dāng) <= 0會(huì)進(jìn)入等待狀態(tài)
__block dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
MM_GLOBAL_QUEUE(^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"這里簡(jiǎn)單寫一下用法,可自行實(shí)現(xiàn)生產(chǎn)者、消費(fèi)者");
        sleep(1);
        dispatch_semaphore_signal(semaphore);
    });復(fù)制代碼

3.2 pthread

  • 測(cè)試代碼
__block pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    __block pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

    MM_GLOBAL_QUEUE(^{
        //NSLog(@"線程 0:加鎖");
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);
        NSLog(@"線程 0:wait");
        pthread_mutex_unlock(&mutex);
        //NSLog(@"線程 0:解鎖");
    });

    MM_GLOBAL_QUEUE(^{
        //NSLog(@"線程 1:加鎖");
        sleep(3);//3秒發(fā)一次信號(hào)
        pthread_mutex_lock(&mutex);
        NSLog(@"線程 1:signal");
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);
        //NSLog(@"線程 1:加鎖");
    });復(fù)制代碼

4、條件鎖

3.1 NSCodition

NSCondition 的對(duì)象實(shí)際上作為一個(gè)鎖和一個(gè)線程檢查器:鎖主要為了當(dāng)檢測(cè)條件時(shí)保護(hù)數(shù)據(jù)源,執(zhí)行條件引發(fā)的任務(wù);線程檢查器主要是根據(jù)條件決定是否繼續(xù)運(yùn)行線程,即線程是否被阻塞。

  • NSCondition同樣實(shí)現(xiàn)了NSLocking協(xié)議,所以它和NSLock一樣,也有NSLocking協(xié)議的lock和unlock方法,可以當(dāng)做NSLock來使用解決線程同步問題,用法完全一樣。
- (void)getIamgeName:(NSMutableArray *)imageNames{
      NSCondition *lock = [[NSCondition alloc] init];
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    [lock unlock];
}復(fù)制代碼

  • 同時(shí),NSCondition提供更高級(jí)的用法。wait和signal,和條件信號(hào)量類似。比如我們要監(jiān)聽imageNames數(shù)組的個(gè)數(shù),當(dāng)imageNames的個(gè)數(shù)大于0的時(shí)候就執(zhí)行清空操作。思路是這樣的,當(dāng)imageNames個(gè)數(shù)大于0時(shí)執(zhí)行清空操作,否則,wait等待執(zhí)行清空操作。當(dāng)imageNames個(gè)數(shù)增加的時(shí)候發(fā)生signal信號(hào),讓等待的線程喚醒繼續(xù)執(zhí)行。

  • NSCondition和NSLock、@synchronized等是不同的是,NSCondition可以給每個(gè)線程分別加鎖,加鎖后不影響其他線程進(jìn)入臨界區(qū)。這是非常強(qiáng)大。
    但是正是因?yàn)檫@種分別加鎖的方式,NSCondition使用wait并使用加鎖后并不能真正的解決資源的競(jìng)爭(zhēng)。比如我們有個(gè)需求:不能讓m<0。假設(shè)當(dāng)前m=0,線程A要判斷到m>0為假,執(zhí)行等待;線程B執(zhí)行了m=1操作,并喚醒線程A執(zhí)行m-1操作的同時(shí)線程C判斷到m>0,因?yàn)樗麄冊(cè)诓煌木€程鎖里面,同樣判斷為真也執(zhí)行了m-1,這個(gè)時(shí)候線程A和線程C都會(huì)執(zhí)行m-1,但是m=1,結(jié)果就會(huì)造成m=-1.

  • 當(dāng)我用數(shù)組做刪除試驗(yàn)時(shí),做增刪操作并不是每次都會(huì)出現(xiàn),大概3-4次后會(huì)出現(xiàn)。單純的使用lock、unlock是沒有問題的。

- (void)executeNSCondition {
    NSCondition* lock = [[NSCondition alloc] init];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (NSUInteger i=0; i<3; i++) {
            sleep(2);
            if (i == 2) {
                [lock lock];
                [lock broadcast];
                [lock unlock];
            }

        }
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCodition:lock];
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCodition:lock];
    });

}

-(void)threadMethodOfNSCodition:(NSCondition*)lock{
    [lock lock];
    [lock wait];
    [lock unlock];

}復(fù)制代碼

3.2 NSCoditionLock

  • lock不分條件,如果鎖沒被申請(qǐng),直接執(zhí)行代碼

  • unlock不會(huì)清空條件,之后滿足條件的鎖還會(huì)執(zhí)行

  • unlockWithCondition:我的理解就是設(shè)置解鎖條件(同一時(shí)刻只有一個(gè)條件,如果已經(jīng)設(shè)置條件,相當(dāng)于修改條件)

  • lockWhenCondition:滿足特定條件,執(zhí)行相應(yīng)代碼

  • NSConditionLock同樣實(shí)現(xiàn)了NSLocking協(xié)議,試驗(yàn)過程中發(fā)現(xiàn)性能很低。

  • NSConditionLock也可以像NSCondition一樣做多線程之間的任務(wù)等待調(diào)用,而且是線程安全的。

- (void)executeNSConditionLock {
    NSConditionLock* lock = [[NSConditionLock alloc] init];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (NSUInteger i=0; i<3; i++) {
            sleep(2);
            if (i == 2) {
                [lock lock];
                [lock unlockWithCondition:i];
            }

        }
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCoditionLock:lock];
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCoditionLock:lock];
    });

}

-(void)threadMethodOfNSCoditionLock:(NSConditionLock*)lock{
    [lock lockWhenCondition:2];
    [lock unlock];

}復(fù)制代碼

3.3 POSIX Conditions

  • POSIX條件鎖需要互斥鎖和條件兩項(xiàng)來實(shí)現(xiàn),雖然看起來沒有什么關(guān)系,但在運(yùn)行時(shí)中,互斥鎖將會(huì)與條件結(jié)合起來。線程將被一個(gè)互斥和條件結(jié)合的信號(hào)來喚醒。

  • 首先初始化條件和互斥鎖,當(dāng)ready_to_gofalse的時(shí)候,進(jìn)入循環(huán),然后線程將會(huì)被掛起,直到另一個(gè)線程將ready_to_go設(shè)置為true的時(shí)候,并且發(fā)送信號(hào)的時(shí)候,該線程才會(huì)被喚醒。

  • 測(cè)試代碼

pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean     ready_to_go = true;
void MyCondInitFunction()
{
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&condition, NULL);
}
void MyWaitOnConditionFunction()
{
    // Lock the mutex.
    pthread_mutex_lock(&mutex);
    // If the predicate is already set, then the while loop is bypassed;
    // otherwise, the thread sleeps until the predicate is set.
    while(ready_to_go == false)
    {
        pthread_cond_wait(&condition, &mutex);
    }
    // Do work. (The mutex should stay locked.)
    // Reset the predicate and release the mutex.
    ready_to_go = false;
    pthread_mutex_unlock(&mutex);
}
void SignalThreadUsingCondition()
{
    // At this point, there should be work for the other thread to do.
    pthread_mutex_lock(&mutex);
    ready_to_go = true;
    // Signal the other thread to begin work.
    pthread_cond_signal(&condition);
    pthread_mutex_unlock(&mutex);
}復(fù)制代碼

5、分布式鎖

分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。在分布式系統(tǒng)中,常常需要協(xié)調(diào)他們的動(dòng)作。如果不同的系統(tǒng)或是同一個(gè)系統(tǒng)的不同主機(jī)之間共享了一個(gè)或一組資源,那么訪問這些資源的時(shí)候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分布式鎖。

5.1 NSDistributedLock

  • 處理多個(gè)進(jìn)程或多個(gè)程序之間互斥問題。

  • 一個(gè)獲取鎖的進(jìn)程或程序在是否鎖之前掛掉,鎖不會(huì)被釋放,可以通過breakLock方式解鎖。

  • iOS很少用到,暫不詳細(xì)研究。

6、讀寫鎖

讀寫鎖實(shí)際是一種特殊的自旋鎖,它把對(duì)共享資源的訪問者劃分成讀者和寫者,讀者只對(duì)共享資源進(jìn)行讀訪問,寫者則需要對(duì)共享資源進(jìn)行寫操作。這種鎖相對(duì)于自旋鎖而言,能提高并發(fā)性,因?yàn)樵诙嗵幚砥飨到y(tǒng)中,它允許同時(shí)有多個(gè)讀者來訪問共享資源,最大可能的讀者數(shù)為實(shí)際的邏輯CPU數(shù)。寫者是排他性的,一個(gè)讀寫鎖同時(shí)只能有一個(gè)寫者或多個(gè)讀者(與CPU數(shù)相關(guān)),但不能同時(shí)既有讀者又有寫者。

6.1 dispatch_barrier_async / dispatch_barrier_sync??

  • 先來一個(gè)需求:假設(shè)我們?cè)扔?個(gè)任務(wù)要執(zhí)行,我們現(xiàn)在要插入一個(gè)任務(wù)0,這個(gè)任務(wù)0要在1、2、4都并發(fā)執(zhí)行完之后才能執(zhí)行,而4、5、6號(hào)任務(wù)要在這幾個(gè)任務(wù)0結(jié)束后才允許并發(fā)。大致的意思如下圖
image.png
  • 直接上代碼:
- (void)rwLockOfBarrier {
    dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"test1");
    });
    dispatch_async(queue, ^{
        NSLog(@"test2");
    });
    dispatch_async(queue, ^{
        NSLog(@"test3");
    });
    dispatch_barrier_sync(queue, ^{
        for (int i = 0; i <= 500000000; i++) {
            if (5000 == i) {
                NSLog(@"point1");
            }else if (6000 == i) {
                NSLog(@"point2");
            }else if (7000 == i) {
                NSLog(@"point3");
            }
        }
        NSLog(@"barrier");
    });
    NSLog(@"aaa");
    dispatch_async(queue, ^{
        NSLog(@"test4");
    });
    dispatch_async(queue, ^{
        NSLog(@"test5");
    });
    dispatch_async(queue, ^{
        NSLog(@"test6");
    });
}復(fù)制代碼

  • 共同點(diǎn):1、等待在它前面插入隊(duì)列的任務(wù)先執(zhí)行完;2、等待他們自己的任務(wù)執(zhí)行完再執(zhí)行后面的任務(wù)。

  • 不同點(diǎn):1、dispatch_barrier_sync將自己的任務(wù)插入到隊(duì)列的時(shí)候,需要等待自己的任務(wù)結(jié)束之后才會(huì)繼續(xù)插入被寫在它后面的任務(wù),然后執(zhí)行它們;2、dispatch_barrier_async將自己的任務(wù)插入到隊(duì)列之后,不會(huì)等待自己的任務(wù)結(jié)束,它會(huì)繼續(xù)把后面的任務(wù)插入隊(duì)列,然后等待自己的任務(wù)結(jié)束后才執(zhí)行后面的任務(wù)。

6.2 pthread

  • 與上述初始化方式類似,靜態(tài)THREAD_RWLOCK_INITIALIZER、動(dòng)態(tài)pthread_rwlock_init()、pthread_rwlock_destroy用來銷毀該鎖
#import <pthread.h>

    __block pthread_rwlock_t rwlock;
    pthread_rwlock_init(&rwlock,NULL);

    //讀
    MM_GLOBAL_QUEUE(^{
        //NSLog(@"線程0:隨眠 1 秒");//還是不打印能直觀些
        sleep(1);
        NSLog(@"線程0:加鎖");
        pthread_rwlock_rdlock(&rwlock);
        NSLog(@"線程0:讀");
        pthread_rwlock_unlock(&rwlock);
        NSLog(@"線程0:解鎖");
    });
    //寫
    MM_GLOBAL_QUEUE(^{
        //NSLog(@"線程1:隨眠 3 秒");
        sleep(3);
        NSLog(@"線程1:加鎖");
        pthread_rwlock_wrlock(&rwlock);
        NSLog(@"線程1:寫");
        pthread_rwlock_unlock(&rwlock);
        NSLog(@"線程1:解鎖");
    });復(fù)制代碼

7、自旋鎖

何謂自旋鎖?它是為實(shí)現(xiàn)保護(hù)共享資源而提出一種鎖機(jī)制。其實(shí),自旋鎖與互斥鎖比較類似,它們都是為了解決對(duì)某項(xiàng)資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時(shí)刻,最多只能有一個(gè)保持者,也就說,在任何時(shí)刻最多只能有一個(gè)執(zhí)行單元獲得鎖。但是兩者在調(diào)度機(jī)制上略有不同。對(duì)于互斥鎖,如果資源已經(jīng)被占用,資源申請(qǐng)者只能進(jìn)入睡眠狀態(tài)。但是自旋鎖不會(huì)引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖,"自旋"一詞就是因此而得名。

7.1 OSSpinLock

  • 使用方式
// 初始化
spinLock = OS_SPINKLOCK_INIT;
// 加鎖
OSSpinLockLock(&spinLock);
// 解鎖
OSSpinLockUnlock(&spinLock);復(fù)制代碼

然而,YYKit作者的文章不再安全的 OSSpinLock有說到這個(gè)自旋鎖存在優(yōu)先級(jí)反轉(zhuǎn)的問題。

7.2 os_unfair_lock

  • 自旋鎖已經(jīng)不再安全,然后蘋果又整出來個(gè) os_unfair_lock_t ,這個(gè)鎖解決了優(yōu)先級(jí)反轉(zhuǎn)的問題。
    os_unfair_lock_t unfairLock;
    unfairLock = &(OS_UNFAIR_LOCK_INIT);
    os_unfair_lock_lock(unfairLock);
    os_unfair_lock_unlock(unfairLock);復(fù)制代碼

8、atomic(property) set / get

利用set / get 接口的屬性實(shí)現(xiàn)原子操作,進(jìn)而確保“被共享”的變量在多線程中讀寫安全,這已經(jīng)是不能滿足部分多線程同步要求。

  • 在定義 property 的時(shí)候, 有atomicnonatomic的屬性修飾關(guān)鍵字。

  • 對(duì)于atomic的屬性,系統(tǒng)生成的 getter/setter 會(huì)保證 get、set 操作的完整性,不受其他線程影響。比如,線程 A 的 getter 方法運(yùn)行到一半,線程 B 調(diào)用了 setter:那么線程 A 的 getter 還是能得到一個(gè)完好無損的對(duì)象。

  • nonatomic就沒有這個(gè)保證了。所以,nonatomic的速度要比atomic快。

raw3d

Atomic

  • 是默認(rèn)的
  • 會(huì)保證 CPU 能在別的線程來訪問這個(gè)屬性之前,先執(zhí)行完當(dāng)前流程
  • 速度不快,因?yàn)橐WC操作整體完成

Non-Atomic

  • 不是默認(rèn)的
  • 更快
  • 線程不安全
  • 如有兩個(gè)線程訪問同一個(gè)屬性,會(huì)出現(xiàn)無法預(yù)料的結(jié)果

Vijayendra Tripathi

  • 假設(shè)有一個(gè) atomic 的屬性 “name”,如果線程 A 調(diào)[self setName:@"A"],線程 B 調(diào)[self setName:@"B"],線程 C 調(diào)`[self name]``,那么所有這些不同線程上的操作都將依次順序執(zhí)行——也就是說,如果一個(gè)線程正在執(zhí)行 getter/setter,其他線程就得等待。因此,屬性 name 是讀/寫安全的。

  • 但是,如果有另一個(gè)線程 D 同時(shí)在調(diào)[name release],那可能就會(huì)crash,因?yàn)?release 不受 getter/setter 操作的限制。也就是說,這個(gè)屬性只能說是讀/寫安全的,但并不是線程安全的,因?yàn)閯e的線程還能進(jìn)行讀寫之外的其他操作。線程安全需要開發(fā)者自己來保證。

  • 如果 name 屬性是 nonatomic 的,那么上面例子里的所有線程 A、B、C、D 都可以同時(shí)執(zhí)行,可能導(dǎo)致無法預(yù)料的結(jié)果。如果是 atomic 的,那么 A、B、C 會(huì)串行,而 D 還是并行的。

  • 簡(jiǎn)單來說,就是atomic會(huì)加一個(gè)鎖來保障線程安全,并且引用計(jì)數(shù)會(huì)+1,來向調(diào)用者保證這個(gè)對(duì)象會(huì)一直存在。假如不這樣做,如果另一個(gè)線程調(diào)setter,可能會(huì)出現(xiàn)線程競(jìng)態(tài),導(dǎo)致引用計(jì)數(shù)降到0,原來那個(gè)對(duì)象就是否了。

9、ONCE

9.1 GCD

  • 多用于創(chuàng)建單例。
+ (instancetype) sharedInstance {
    static id __instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        __instance = [[self alloc] init];
    });
    return __instance;
}復(fù)制代碼

9.2 pthread

  • 廢話不多說,直接上代碼
// 定義方法
void fun() {
    NSLog(@"%@", [NSThread currentThread]);
}

- (void)onceOfPthread {
    __block pthread_once_t once = PTHREAD_ONCE_INIT;

    int i= 0;
    while (i > 5) {
        pthread_once(&once, fun);
        i++;
    }

}復(fù)制代碼

三、性能對(duì)比

基礎(chǔ)表現(xiàn)-所操作耗時(shí)

image.png

上圖是常規(guī)的鎖操作性能測(cè)試(iOS7.0SDK,iPhone6模擬器,Yosemite 10.10.5),垂直方向表示耗時(shí),單位是秒,總耗時(shí)越小越好,水平方向表示不同類型鎖的鎖操作,具體又分為兩部分,左邊的常規(guī)lock操作(比如NSLock)或者讀read操作(比如ANReadWriteLock),右邊則是寫write操作,圖上僅有ANReadWriteLockANRecursiveRWLock支持,其它不支持的則默認(rèn)為0,圖上看出,單從性能表現(xiàn),原子操作是表現(xiàn)最佳的(0.057412秒),@synchronized則是最耗時(shí)的(1.753565秒) (測(cè)試代碼) 。

多線程鎖刪除數(shù)組性能測(cè)試

  • 模擬器環(huán)境:i5 2.6GH+8G 內(nèi)存,xcode 7.2.1 (7C1002)+iPhone6SP(9.2)
image.png
  • 真機(jī)環(huán)境:xcode 7.2.1 (7C1002)+iPhone6(國(guó)行)
image.png
  • 通過測(cè)試發(fā)現(xiàn)模擬器和真機(jī)的區(qū)別還是很大的,模擬器上明顯的階梯感,真機(jī)就沒有,模擬器上NSConditionLock的性能非常差,我沒有把它的參數(shù)加在表格上,不然其他的就看不到了。不過真機(jī)上面性能還好。

  • 這些性能測(cè)試只是一個(gè)參考,沒必要非要去在意這些,畢竟前端的編程一般線程要求沒那么高,可以從其他的地方優(yōu)化。線程安全中注意避坑,另外選擇自己喜歡的方式,這樣你可以研究的更深入,使用的更熟練。

聲明: 測(cè)試結(jié)果僅僅代表一個(gè)參考,因?yàn)楦鞣N因素的影響,并沒有那么準(zhǔn)確。

綜合比較

image.png

可以看到除了 OSSpinLock 外,dispatch_semaphorepthread_mutex 性能是最高的。有消息稱,蘋果在新的系統(tǒng)中已經(jīng)優(yōu)化了 pthread_mutex 的性能,所有它看上去和 dispatch_semaphore 差距并沒有那么大了。

四、常見的死鎖

首先要明確幾個(gè)概念

1.串行與并行

在使用GCD的時(shí)候,我們會(huì)把需要處理的任務(wù)放到Block中,然后將任務(wù)追加到相應(yīng)的隊(duì)列里面,這個(gè)隊(duì)列,叫做 Dispatch Queue。然而,存在于兩種Dispatch Queue,一種是要等待上一個(gè)任務(wù)執(zhí)行完,再執(zhí)行下一個(gè)的Serial Dispatch Queue,這叫做串行隊(duì)列;另一種,則是不需要上一個(gè)任務(wù)執(zhí)行完,就能執(zhí)行下一個(gè)的ConcurrentDispatch Queue,叫做并行隊(duì)列。這兩種,均遵循FIFO原則。

舉一個(gè)簡(jiǎn)單的例子,在三個(gè)任務(wù)中輸出1、2、3,串行隊(duì)列輸出是有序的1、2、3,但是并行隊(duì)列的先后順序就不一定了。

雖然可以同時(shí)多個(gè)任務(wù)的處理,但是并行隊(duì)列的處理量,還是要根據(jù)當(dāng)前系統(tǒng)狀態(tài)來。如果當(dāng)前系統(tǒng)狀態(tài)最多處理2個(gè)任務(wù),那么1、2會(huì)排在前面,3什么時(shí)候操作,就看1或者2誰先完成,然后3接在后面。

串行和并行就簡(jiǎn)單說到這里,關(guān)于它們的技術(shù)點(diǎn)其實(shí)還有很多,可以自行了解。

2.同步與異步

串行與并行針對(duì)的是隊(duì)列,而同步與異步,針對(duì)的則是線程。最大的區(qū)別在于,同步線程要阻塞當(dāng)前線程,必須要等待同步線程中的任務(wù)執(zhí)行完,返回以后,才能繼續(xù)執(zhí)行下一個(gè)任務(wù);而異步線程則是不用等待。

3.GCD API

GCD API很多,這里僅介紹本文用到的。

    1. 系統(tǒng)提供的兩個(gè)隊(duì)列
// 全局隊(duì)列,也是一個(gè)并行隊(duì)列
dispatch_get_global_queue
// 主隊(duì)列,在主線程中運(yùn)行,因?yàn)橹骶€程只有一個(gè),所有這是一個(gè)串行隊(duì)列
dispatch_get_main_queue復(fù)制代碼

    1. 除此之外,還可以自己生成隊(duì)列
// 從DISPATCH_QUQUE_SERIAL看出,這是串行隊(duì)列
dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL)
// 同理,這是一個(gè)并行隊(duì)列
dispatch_queue_create("com.demo.concurrentQueue", DISPATCH_QUEUE_CONCURRENT)復(fù)制代碼

    1. 接下來是同步與異步線程的創(chuàng)造
dispatch_sync(..., ^(block)) // 同步線程
dispatch_async(..., ^(block)) // 異步線程復(fù)制代碼

案例分析

案例一
NSLog(@"1"); // 任務(wù)1
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"2"); // 任務(wù)2
});
NSLog(@"3"); // 任務(wù)3復(fù)制代碼

  • 結(jié)果,控制臺(tái)輸出:
1復(fù)制代碼

分析

    1. dispatch_sync表示一個(gè)同步線程;
    1. dispatch_get_main_queue表示運(yùn)行在主線程中的主隊(duì)列;
    1. 任務(wù)2是同步線程的任務(wù)。

首先執(zhí)行任務(wù)1,這是肯定沒問題的,只是接下來,程序遇到了同步線程,那么它會(huì)進(jìn)入等待,等待任務(wù)2執(zhí)行完,然后執(zhí)行任務(wù)3。但這是隊(duì)列,有任務(wù)來,當(dāng)然會(huì)將任務(wù)加到隊(duì)尾,然后遵循FIFO原則執(zhí)行任務(wù)。那么,現(xiàn)在任務(wù)2就會(huì)被加到最后,任務(wù)3排在了任務(wù)2前面,問題來了:

任務(wù)3要等任務(wù)2執(zhí)行完才能執(zhí)行,任務(wù)2由排在任務(wù)3后面,意味著任務(wù)2要在任務(wù)3執(zhí)行完才能執(zhí)行,所以他們進(jìn)入了互相等待的局面。【既然這樣,那干脆就卡在這里吧】這就是死鎖。

image.png
案例二
NSLog(@"1"); // 任務(wù)1
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSLog(@"2"); // 任務(wù)2
});
NSLog(@"3"); // 任務(wù)3復(fù)制代碼

  • 結(jié)果,控制臺(tái)輸出:
1
2
3復(fù)制代碼

分析

首先執(zhí)行任務(wù)1,接下來會(huì)遇到一個(gè)同步線程,程序會(huì)進(jìn)入等待。等待任務(wù)2執(zhí)行完成以后,才能繼續(xù)執(zhí)行任務(wù)3。從dispatch_get_global_queue可以看出,任務(wù)2被加入到了全局的并行隊(duì)列中,當(dāng)并行隊(duì)列執(zhí)行完任務(wù)2以后,返回到主隊(duì)列,繼續(xù)執(zhí)行任務(wù)3。

image.png
案例三
dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1"); // 任務(wù)1
dispatch_async(queue, ^{
    NSLog(@"2"); // 任務(wù)2
    dispatch_sync(queue, ^{  
        NSLog(@"3"); // 任務(wù)3
    });
    NSLog(@"4"); // 任務(wù)4
});
NSLog(@"5"); // 任務(wù)5復(fù)制代碼

  • 結(jié)果,控制臺(tái)輸出:
1
5
2
// 5和2的順序不一定復(fù)制代碼

分析

這個(gè)案例沒有使用系統(tǒng)提供的串行或并行隊(duì)列,而是自己通過dispatch_queue_create函數(shù)創(chuàng)建了一個(gè)DISPATCH_QUEUE_SERIAL的串行隊(duì)列。

    1. 執(zhí)行任務(wù)1;
    1. 遇到異步線程,將【任務(wù)2、同步線程、任務(wù)4】加入串行隊(duì)列中。因?yàn)槭钱惒骄€程,所以在主線程中的任務(wù)5不必等待異步線程中的所有任務(wù)完成;
    1. 因?yàn)槿蝿?wù)5不必等待,所以2和5的輸出順序不能確定;
    1. 任務(wù)2執(zhí)行完以后,遇到同步線程,這時(shí),將任務(wù)3加入串行隊(duì)列;
    1. 又因?yàn)槿蝿?wù)4比任務(wù)3早加入串行隊(duì)列,所以,任務(wù)3要等待任務(wù)4完成以后,才能執(zhí)行。但是任務(wù)3所在的同步線程會(huì)阻塞,所以任務(wù)4必須等任務(wù)3執(zhí)行完以后再執(zhí)行。這就又陷入了無限的等待中,造成死鎖。
image.png
案例四
NSLog(@"1"); // 任務(wù)1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"2"); // 任務(wù)2
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"3"); // 任務(wù)3
    });
    NSLog(@"4"); // 任務(wù)4
});
NSLog(@"5"); // 任務(wù)5復(fù)制代碼

  • 結(jié)果,控制臺(tái)輸出:
1
2
5
3
4
// 5和2的順序不一定復(fù)制代碼

分析

首先,將【任務(wù)1、異步線程、任務(wù)5】加入Main Queue中,異步線程中的任務(wù)是:【任務(wù)2、同步線程、任務(wù)4】。

所以,先執(zhí)行任務(wù)1,然后將異步線程中的任務(wù)加入到Global Queue中,因?yàn)楫惒骄€程,所以任務(wù)5不用等待,結(jié)果就是2和5的輸出順序不一定。

然后再看異步線程中的任務(wù)執(zhí)行順序。任務(wù)2執(zhí)行完以后,遇到同步線程。將同步線程中的任務(wù)加入到Main Queue中,這時(shí)加入的任務(wù)3在任務(wù)5的后面。

當(dāng)任務(wù)3執(zhí)行完以后,沒有了阻塞,程序繼續(xù)執(zhí)行任務(wù)4。

從以上的分析來看,得到的幾個(gè)結(jié)果:1最先執(zhí)行;2和5順序不一定;4一定在3后面。

image.png
案例五
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"1"); // 任務(wù)1
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2"); // 任務(wù)2
    });
    NSLog(@"3"); // 任務(wù)3
});
NSLog(@"4"); // 任務(wù)4
while (1) {
}
NSLog(@"5"); // 任務(wù)5復(fù)制代碼

  • 結(jié)果,控制臺(tái)輸出:
1
4
// 1和4的順序不一定復(fù)制代碼

分析

和上面幾個(gè)案例的分析類似,先來看看都有哪些任務(wù)加入了Main Queue:【異步線程、任務(wù)4、死循環(huán)、任務(wù)5】。

在加入到Global Queue異步線程中的任務(wù)有:【任務(wù)1、同步線程、任務(wù)3】。

第一個(gè)就是異步線程,任務(wù)4不用等待,所以結(jié)果任務(wù)1和任務(wù)4順序不一定。

任務(wù)4完成后,程序進(jìn)入死循環(huán),Main Queue阻塞。但是加入到Global Queue的異步線程不受影響,繼續(xù)執(zhí)行任務(wù)1后面的同步線程。

同步線程中,將任務(wù)2加入到了主線程,并且,任務(wù)3等待任務(wù)2完成以后才能執(zhí)行。這時(shí)的主線程,已經(jīng)被死循環(huán)阻塞了。所以任務(wù)2無法執(zhí)行,當(dāng)然任務(wù)3也無法執(zhí)行,在死循環(huán)后的任務(wù)5也不會(huì)執(zhí)行。

最終,只能得到1和4順序不定的結(jié)果。


image.png

五、總結(jié)

    1. 總的來看,推薦pthread_mutex作為實(shí)際項(xiàng)目的首選方案;
    1. 對(duì)于耗時(shí)較大又易沖突的讀操作,可以使用讀寫鎖代替pthread_mutex;
    1. 如果確認(rèn)僅有set/get的訪問操作,可以選用原子操作屬性;
    1. 對(duì)于性能要求苛刻,可以考慮使用OSSpinLock,需要確保加鎖片段的耗時(shí)足夠??;
    1. 條件鎖基本上使用面向?qū)ο蟮腘SCondition和NSConditionLock即可;
    1. @synchronized則適用于低頻場(chǎng)景如初始化或者緊急修復(fù)使用;

蘋果為多線程、共享內(nèi)存提供了多種同步解決方案(鎖),對(duì)于這些方案的比較,大都討論了鎖的用法以及鎖操作的開銷。個(gè)人認(rèn)為最優(yōu)秀的選用還是看應(yīng)用場(chǎng)景,高頻接口VS低頻接口、有限沖突VS激烈競(jìng)爭(zhēng)、代碼片段耗時(shí)的長(zhǎng)短,都是選擇的重要依據(jù),選擇適用于當(dāng)前應(yīng)用場(chǎng)景的方案才是王道。

如果有錯(cuò)誤或者不足的地方請(qǐng)指正,

作為一個(gè)開發(fā)者,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這是我的iOS交流圈:
不管你是小白還是大牛歡迎入駐!!
分享內(nèi)容包括逆向安防、算法、架構(gòu)設(shè)計(jì)、多線程,網(wǎng)絡(luò)進(jìn)階,還有底層、音視頻、Flutter等等…
自己根據(jù)梳理網(wǎng)絡(luò)來的的開發(fā)經(jīng)驗(yàn)總結(jié)的學(xué)習(xí)方法,無償分享給大家。需要的話都可以自行來獲取下載。
+裙:196800191、 或者是+ WX(XiAZHiGardenia)
或者是+ WX(XiAZHiGardenia)免費(fèi)獲??! 獲取面試資料 簡(jiǎn)歷模板 一起交流技術(shù)

作者:喵渣渣
原文鏈接:https://juejin.cn/post/6844903511147167751

參考文檔

關(guān)注下面的標(biāo)簽,發(fā)現(xiàn)更多相似文章

?著作權(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)容