iOS-多線程3-加鎖方案2

一. NSConditionLock

NSConditionLock是對NSCondition的進一步封裝纵揍,可以設(shè)置具體的條件值囤躁。

NSConditionLock相關(guān)API:

@interface NSConditionLock : NSObject <NSLocking> {

- (instancetype)initWithCondition:(NSInteger)condition;

@property (readonly) NSInteger condition; //條件值

- (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;

@property (nullable, copy) NSString *name;

@end

簡單使用如下:

#import "NSConditionLockDemo.h"

@interface NSConditionLockDemo()
@property (strong, nonatomic) NSConditionLock *conditionLock;
@end

@implementation NSConditionLockDemo

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

- (void)otherTest
{
    //線程1
    [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
    //線程2
    [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
    //線程3
    [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start];
}

- (void)__one
{
    [self.conditionLock lockWhenCondition:1];
    
    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];
}
@end

可以發(fā)現(xiàn),三個子線程同時執(zhí)行代碼邢疙,最后打印結(jié)果是:

__one
__two
__three

先執(zhí)行線程1棍弄,再執(zhí)行線程2,再執(zhí)行線程3疟游,達到線程3依賴線程2呼畸,線程2依賴線程1的效果。

使用場景:

如果子線程有依賴關(guān)系(子線程的執(zhí)行是有順序的)颁虐,就可以使用NSConditionLock役耕,設(shè)置條件具體的值。

在GNUstep中查看源碼:

- (id) init
{
    return [self initWithCondition: 0];
}

- (id) initWithCondition: (NSInteger)value
{
    if (nil != (self = [super init]))
    {
        if (nil == (_condition = [NSCondition new]))
        {
            DESTROY(self);
        }
        else
        {
            _condition_value = value;
            [_condition setName:
             [NSString stringWithFormat: @"condition-for-lock-%p", self]];
        }
    }
    return self;
}

可以發(fā)現(xiàn)兩個問題:

  1. 條件值默認是0
  2. NSConditionLock的確是對NSCondition的封裝

二. dispatch_queue(DISPATCH_QUEUE_SERIAL)

直接使用GCD的串行隊列聪廉,也是可以實現(xiàn)線程同步的

#import "SerialQueueDemo.h"

@interface SerialQueueDemo()
@property (strong, nonatomic) dispatch_queue_t ticketQueue;
@property (strong, nonatomic) dispatch_queue_t moneyQueue;
@end

@implementation SerialQueueDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);
        self.moneyQueue = dispatch_queue_create("moneyQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)__drawMoney
{
    dispatch_sync(self.moneyQueue, ^{
        [super __drawMoney];
    });
}

- (void)__saveMoney
{
    dispatch_sync(self.moneyQueue, ^{
        [super __saveMoney];
    });
}

- (void)__saleTicket
{
    dispatch_sync(self.ticketQueue, ^{
        [super __saleTicket];
    });
}
@end

dispatch_sync函數(shù)的特點:要求立馬在當前線程同步執(zhí)行任務(wù)(當前線程是子線程瞬痘,在MJBaseDemo里面已經(jīng)寫了)。

本來這個方法就是在子線程中執(zhí)行的板熊,把存錢框全、取錢操作放到一個串行隊列里面,把賣票操作放到另一個串行隊列里面干签。

舉例說明:比如線程4進來賣票津辩,那么這個操作就會被放到串行隊列中,等一會線程7又進來賣票容劳,這個操作也會被放到串行隊列中喘沿,串行隊列里面的東西是:線程4的賣票操作 - 線程7的賣票操作 - 線程5的賣票操作。

這樣線程4賣完竭贩,線程7賣蚜印,線程7賣完,線程5賣留量。這樣串行隊列中的任務(wù)是異步的窄赋,不會出現(xiàn)多個線程同時訪問一個成員變量的問題,這樣也能解決線程安全問題楼熄。

所以說忆绰,線程同步問題也不是必須要通過加鎖才能實現(xiàn)。

三. dispatch_semaphore

  1. semaphore叫做”信號量”
  2. 信號量的初始值可岂,可以用來控制線程并發(fā)訪問的最大數(shù)量
  3. 信號量的初始值為1错敢,代表同時只允許1條線程訪問資源,保證線程同步

如下代碼缕粹,創(chuàng)建15條線程稚茅,都調(diào)用test方法:

@interface SemaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@end

@implementation SemaphoreDemo
- (void)viewDidLoad {
   [super viewDidLoad]

    self.semaphore = dispatch_semaphore_create(5);

    for (int i = 0; i < 15; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
    }
}

// 線程10、7致开、6峰锁、9、8
- (void)test
{
    // 如果信號量的值 > 0双戳,就讓信號量的值減1虹蒋,然后繼續(xù)往下執(zhí)行代碼
    // 如果信號量的值 <= 0,就會休眠等待飒货,直到信號量的值變成>0魄衅,就讓信號量的值減1,然后繼續(xù)往下執(zhí)行代碼
    // 第二個參數(shù)代表等到啥時候塘辅,傳入的DISPATCH_TIME_FOREVER晃虫,代表一直等
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(2);
    NSLog(@"test - %@", [NSThread currentThread]);
    
    // 讓信號量的值+1
    dispatch_semaphore_signal(self.semaphore);
}
@end

打印:

test - <NSThread: 0x600003a59380>{number = 7, name = (null)}
test - <NSThread: 0x600003a59400>{number = 9, name = (null)}
test - <NSThread: 0x600003a593c0>{number = 8, name = (null)}
test - <NSThread: 0x600003a59440>{number = 10, name = (null)}
test - <NSThread: 0x600003a59140>{number = 4, name = (null)}
    間隔2秒
test - <NSThread: 0x600003a594c0>{number = 12, name = (null)}
test - <NSThread: 0x600003a59480>{number = 11, name = (null)}
test - <NSThread: 0x600003a59500>{number = 13, name = (null)}
test - <NSThread: 0x600003a59300>{number = 5, name = (null)}
test - <NSThread: 0x600003a59540>{number = 14, name = (null)}
    間隔2秒
test - <NSThread: 0x600003a59580>{number = 15, name = (null)}
test - <NSThread: 0x600003a595c0>{number = 16, name = (null)}
test - <NSThread: 0x600003a59600>{number = 17, name = (null)}
test - <NSThread: 0x600003a59340>{number = 6, name = (null)}
test - <NSThread: 0x600003a59100>{number = 3, name = (null)}

上面代碼扣墩,創(chuàng)建了15條線程哲银,如果不控制線程并發(fā)訪問的最大數(shù)量扛吞,那么有可能15條線程同時訪問test方法,這樣就不安全荆责。使用了信號量滥比,發(fā)現(xiàn)最多只有5條線程訪問test,這5條訪問完做院,后面5條線程繼續(xù)訪問盲泛。

所以,如果信號量的初始值為1键耕,代表同時只允許1條線程訪問資源寺滚,保證線程同步,代碼如下:

#import "SemaphoreDemo.h"

@interface SemaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t ticketSemaphore;
@property (strong, nonatomic) dispatch_semaphore_t moneySemaphore;
@end

@implementation SemaphoreDemo

- (instancetype)init
{
    if (self = [super init]) {
        //設(shè)置信號量的初始值為1屈雄,代表同時只允許1條線程訪問資源村视,保證線程同步
        self.ticketSemaphore = dispatch_semaphore_create(1);
        self.moneySemaphore = dispatch_semaphore_create(1);
    }
    return self;
}

- (void)__drawMoney
{
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
    
    [super __drawMoney];
    
    dispatch_semaphore_signal(self.moneySemaphore);
}

- (void)__saveMoney
{
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
    
    [super __saveMoney];
    
    dispatch_semaphore_signal(self.moneySemaphore);
}

- (void)__saleTicket
{
    dispatch_semaphore_wait(self.ticketSemaphore, DISPATCH_TIME_FOREVER);
    
    [super __saleTicket];
    
    dispatch_semaphore_signal(self.ticketSemaphore);
}

信號量原理:

dispatch_semaphore_t ticketSemaphore = dispatch_semaphore_create(1);
//如果信號量的值 > 0,就讓信號量的值減1棚亩,然后繼續(xù)往下執(zhí)行代碼
//如果信號量的值 <= 0蓖议,就會休眠等待,直到信號量的值變成>0讥蟆,就讓信號量的值減1勒虾,然后繼續(xù)往下執(zhí)行代碼
//第二個參數(shù)代表等到啥時候,傳入的DISPATCH_TIME_FOREVER瘸彤,代表一直等
dispatch_semaphore_wait(ticketSemaphore, DISPATCH_TIME_FOREVER);
......
//讓信號量的值+1
dispatch_semaphore_signal(ticketSemaphore);

小提示:控制線程并發(fā)訪問的最大數(shù)量也可以用

NSOperationQueue *queue;
queue.maxConcurrentOperationCount = 5;

四. @synchronized

  1. @synchronized是對mutex遞歸鎖的封裝
  2. @synchronized(obj)內(nèi)部會生成obj對應(yīng)的遞歸鎖修然,然后進行加鎖、解鎖操作

使用如下:

#import "SynchronizedDemo.h"

@implementation SynchronizedDemo

- (void)__drawMoney
{
    //最簡單的一種方式质况,但是性能比較差愕宋,蘋果不推薦使用,所以打出來的時候沒提示结榄。
    //其中()中是拿什么當做一把鎖中贝,比如下面是拿當前類對象當做一把鎖。
    //為什么把類對象當做一把鎖臼朗?因為類對象只有一個邻寿,以后無論什么實例對象調(diào)用這個方法,都是類對象作為鎖视哑,這樣就只有一把鎖绣否,才能鎖住。
    @synchronized([self class]) {
        [super __drawMoney];
    }
}

- (void)__saveMoney
{
    @synchronized([self class]) { // objc_sync_enter
        [super __saveMoney];
    } // objc_sync_exit
}

- (void)__saleTicket
{
    //除了用類對象作為一把鎖挡毅,也可以直接傳入一個其他對象蒜撮,并且要dispatch_once_t,保證以后不管調(diào)用多少次都是同一個對象跪呈。
    static NSObject *lock;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        lock = [[NSObject alloc] init];
    });
    
    @synchronized(lock) {
        [super __saleTicket];
    }
}
@end

在@synchronized處打斷點段磨,查看匯編取逾,可以發(fā)現(xiàn)會調(diào)用objc_sync_enter和objc_sync_exit。就相當于薇溃,第一個“{”是objc_sync_enter菌赖,第二個“}”是objc_sync_exit,如下:

@synchronized([self class]) { // objc_sync_enter
......
} // objc_sync_exit

然后在objc4中的objc-sync.mm文件中查看objc_sync_enter函數(shù)的源碼實現(xiàn):

typedef struct SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  
    recursive_mutex_t mutex; //遞歸鎖
} SyncData;

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {//將傳進來的obj轉(zhuǎn)成data
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();//再從data里面取出一把遞歸鎖沐序,所以只要obj一樣,取出的鎖也一樣
    } else {
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

從上面代碼可以看出堕绩,將傳進來的obj轉(zhuǎn)成data策幼,再從data里面取出一把遞歸鎖,所以只要obj一樣奴紧,取出的鎖也一樣特姐,所以我們上面才說傳入的obj要唯一。

遞歸鎖:允許同一個線程對一把鎖進行重復加鎖(解鎖)

既然@synchronized是遞歸鎖黍氮,那么肯定可以做遞歸鎖可以做的事情唐含,如下:

- (void)otherTest {
    @synchronized([self class]) {
        NSLog(@"123");
        //遞歸調(diào)用10次
        static int count = 0;
        if (count < 10) {
            count++;
            [self otherTest];
        }
    }
}

上面代碼,遞歸調(diào)用10次沫浆,最后打印10次“123”捷枯。如果將@synchronized換成其他鎖,遞歸調(diào)用就會造成死鎖专执,最后結(jié)果打印了10次淮捆,說明的確是遞歸鎖。

注意:@synchronized和@synthesize本股、@dynamic不一樣攀痊,別弄混淆了,關(guān)于@synthesize拄显、@dynamic可參考Runtime3-objc_msgSend底層調(diào)用流程的補充內(nèi)容苟径。

五. 總結(jié):回憶一下前面學的各種鎖

OSSpinLock 自旋鎖,因為底層是使用while循環(huán)進行忙等躬审,不會進行休眠和喚醒棘街,所以是性能比較高的一把鎖,但是現(xiàn)在已經(jīng)不安全盒件,被拋棄蹬碧。

os_unfair_lock 用于取代不安全的OSSpinLock ,從iOS10開始才支持炒刁。從底層調(diào)用看恩沽,等待os_unfair_lock鎖的線程會處于休眠狀態(tài),并非忙等翔始,是一種互斥鎖罗心。

pthread_mutex mutex叫做”互斥鎖”里伯,等待鎖的線程會處于休眠狀態(tài)。
它是跨平臺的渤闷,當傳入的類型是默認的就是默認鎖疾瓮,當傳入PTHREAD_MUTEX_RECURSIVE,就是遞歸鎖飒箭,還可以通過pthread_cond_wait(&_cond, &_mutex)當做條件鎖來使用狼电。

dispatch_semaphore 信號量的初始值為1,代表同時只允許1條線程訪問資源弦蹂,保證線程同步

dispatch_queue(DISPATCH_QUEUE_SERIAL) 直接使用GCD的串行隊列肩碟,也是可以實現(xiàn)線程同步的

NSLock是對mutex普通鎖的封裝
NSRecursiveLock是對mutex遞歸鎖的封裝,API跟NSLock基本一致
NSCondition是對mutex和cond的封裝
NSConditionLock是對NSCondition的進一步封裝凸椿,可以設(shè)置具體的條件值

@synchronized也是對mutex遞歸鎖的封裝
@synchronized(obj)內(nèi)部會生成obj對應(yīng)的遞歸鎖削祈,然后進行加鎖、解鎖操作

六. 各種鎖性能比較

自此脑漫,iOS中各種鎖基本上都講完了髓抑,下面比較一下性能,然后找出一個最好的优幸,留著開發(fā)中使用吨拍。

性能從高到低排序:

os_unfair_lock iOS10開始支持
OSSpinLock 不安全,被拋棄
dispatch_semaphore 如果需要iOS8劈伴、9都支持可以使用
pthread_mutex 可以跨平臺
dispatch_queue(DISPATCH_QUEUE_SERIAL) 本來GCD效率就很高
NSLock 對mutex普通鎖的封裝
NSCondition 對mutex和cond的封裝
pthread_mutex(recursive) mutex遞歸鎖密末,遞歸鎖效率本來就低
NSRecursiveLock 對mutex遞歸鎖的封裝
NSConditionLock 對NSCondition的封裝
@synchronized 對mutex遞歸鎖的封裝

總結(jié):

一般推薦使用os_unfair_lockdispatch_semaphore跛璧、pthread_mutex严里。

使用技巧:

對于dispatch_semaphore來說,如果下面每個方法的鎖都是不一樣的追城,我們可以寫成下面這樣的宏:

#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);

使用如下:

- (void)test1
{
    SemaphoreBegin;
    // .....
    SemaphoreEnd;
}

- (void)test2
{
    SemaphoreBegin;
    // .....
    SemaphoreEnd;
}

- (void)test3
{
    SemaphoreBegin;
    // .....
    SemaphoreEnd;
}

這樣就保證每個方法內(nèi)部的鎖都不一樣刹碾,同時使用起來也很簡單。

如果每個方法的鎖都是一樣的座柱,那就只能把鎖寫到外面去了迷帜。
同理,pthread_mutex也可以這樣封裝色洞。

七. 面試題

  1. 你理解的多線程戏锹?
    就是多條線程同時做事情,優(yōu)點就是效率高火诸,缺點就是可能會造成線程安全問題锦针。

  2. iOS的多線程方案有哪幾種?你更傾向于哪一種?
    多線程方案如下圖奈搜,更傾向于GCD悉盆。

多線程方案.png

① 這些多線程方案的底層都是依賴pthread
② NSThread線程生命周期是程序員管理,GCD和NSOperation是系統(tǒng)自動管理
③ NSThread和NSOperation都是OC的馋吗,更加面向?qū)ο?br> ④ NSOperation基于CGD焕盟,使用更加面向?qū)ο?/p>

  1. 你在項目中用過 GCD 嗎?

  2. GCD 的隊列類型宏粤?

  3. 說一下 OperationQueue 和 GCD 的區(qū)別脚翘,以及各自的優(yōu)勢有哪些?

  4. 線程安全的處理手段有哪些商架?
    上面講的

  5. OC你了解的鎖有哪些堰怨?在你回答基礎(chǔ)上進行二次提問;
    追問一:自旋和互斥對比蛇摸?
    追問二:使用以上鎖需要注意哪些?
    追問三:用C/OC/C++灿巧,任選其一赶袄,實現(xiàn)自旋或互斥?口述即可抠藕!

  6. 自旋鎖饿肺、互斥鎖比較?
    自旋鎖:顧名思義就是自己一直在旋轉(zhuǎn)的鎖盾似,比如上面的OSSpinLock敬辣,它底層是個while循環(huán)。
    互斥鎖:互斥鎖提供一個可以在同一時間零院,只讓一個線程訪問臨界資源的操作接口溉跃。互斥鎖(Mutex)是個提供線程同步的基本鎖告抄。上鎖后撰茎,其他的線程如果想要鎖上,那么會被阻塞(線程休眠)打洼,直到鎖釋放后(說明龄糊,一般會把訪問共享內(nèi)存這段代碼放在上鎖程序之后)———百度百科

上面講的那些鎖,除了OSSpinLock是自旋鎖募疮,其他的都是互斥鎖炫惩。

  1. 什么情況使用自旋鎖比較劃算?
    預計線程等待鎖的時間很短
    加鎖的代碼(臨界區(qū))經(jīng)常被調(diào)用阿浓,但競爭情況很少發(fā)生
    CPU資源不緊張
    多核處理器

  2. 什么情況使用互斥鎖比較劃算他嚷?
    預計線程等待鎖的時間較長
    單核處理器
    臨界區(qū)有IO操作,因為IO操作比較占用CPU資源
    臨界區(qū)代碼復雜或者循環(huán)量大
    臨界區(qū)競爭非常激烈

八. atomic

nonatomic和atomic
atom:原子,不可再分割的單位
atomic:原子性

原子性操作就說明這個操作是個整體爸舒,不可分割的蟋字。
比如如下三行代碼,如果不是原子性操作扭勉,那么這三行代碼就有可能被三個線程執(zhí)行鹊奖,這樣肯定是不行的,那怎么變成原子性操作呢涂炎?
在第一行前面加鎖忠聚,第三行后面解鎖,就變成了原子性操作唱捣,變成原子性操作之后要么不執(zhí)行两蟀,要執(zhí)行必須把三行一塊執(zhí)行完,如下:

 // 加鎖
 int a = 10;
 int b = 20;
 int c = a + b;
 // 解鎖

atomic用于保證屬性setter震缭、getter的原子性操作赂毯,相當于在getter和setter內(nèi)部加了線程同步的鎖,也就是setter和getter內(nèi)部都有加鎖操作拣宰。

在objc4的objc-accessors.mm文件找到如下源碼:

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) { //如果是nonatomic党涕,就直接賦值
        oldValue = *slot;
        *slot = newValue;
    } else { //如果是atomic,就先加鎖后解鎖
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock(); //加鎖
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock(); //解鎖
    }

    objc_release(oldValue);
}

可以發(fā)現(xiàn)巡社,setter方法里面膛堤,如果是nonatomic,就直接賦值晌该,如果是atomic肥荔,就先加鎖后解鎖,getter方法也一樣朝群,就不解釋了燕耿。

對于atomic,上面我們說了setter潜圃、getter方法內(nèi)部會有加鎖缸棵、解鎖操作,但它并不能保證使用屬性的過程是線程安全的谭期,什么意思呢堵第?

如下:

@interface MJPerson : NSObject
@property (strong, atomic) NSMutableArray *data; //atomic修飾
@end

//執(zhí)行以下代碼:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *p = [[MJPerson alloc] init];
        
        for (int i = 0; i < 10; i++) {
            dispatch_async(NULL, ^{
                p.data = [NSMutableArray array];
            });
        }
        
        NSMutableArray *array = p.data;
        // 沒有加鎖
        [array addObject:@"1"];
        [array addObject:@"2"];
        [array addObject:@"3"];
        // 沒有解鎖
    }
    return 0;
}

上面代碼 p.data = [NSMutableArray array] 這一行是線程安全的,因為它的setter方法內(nèi)部有加鎖隧出、解鎖操作踏志。
但是 [array addObject:@"1"] 這一行就不是線程安全的了,因為它沒有加鎖胀瞪、解鎖操作针余。

既然atomic是線程安全的饲鄙,那么為什么開發(fā)中我們基本不用呢?

  1. 太耗性能了圆雁,因為setter忍级、getter方法調(diào)用次數(shù)太頻繁了,如果每次都需要加鎖伪朽、解鎖轴咱,那手機CPU資源不就被你消耗完了。所以atomic一般在MAC上才使用烈涮。

  2. 而且只有多條線程同時訪問同一個對象的屬性朴肺,才會有線程安全問題跷敬。這種情況幾乎沒有辕宏,如果你非要造出來這種情況,比如如下代碼泉沾,多條線程同時訪問 p.data 讶舰,那你完全可以在外面加鎖嘛鞍盗!

for (int i = 0; i < 10; i++) {
    dispatch_async(NULL, ^{
        // 在外面加鎖
        p.data = [NSMutableArray array];
        // 在外面解鎖
    });
}

九. 讀寫安全方案

顯然,如果使用上面講的加鎖方案跳昼,那么無論讀橡疼、寫,同一時間只有一條線程在執(zhí)行庐舟,這樣效率比較低,實際上讀操作可以同時多條線程一起執(zhí)行的住拭。

思考如何實現(xiàn)以下場景
同一時間挪略,只能有1個線程進行寫的操作
同一時間,允許有多個線程進行讀的操作
同一時間滔岳,不允許既有寫的操作杠娱,又有讀的操作

上面的場景就是典型的“多讀單寫”,經(jīng)常用于文件等數(shù)據(jù)的讀寫操作谱煤,iOS中的實現(xiàn)方案有:

  1. pthread_rwlock_t:讀寫鎖
  2. dispatch_barrier_async:異步柵欄調(diào)用

1. pthread_rwlock_t

等待鎖的線程會進入休眠(有點互斥鎖的感覺)

代碼如下:

#import "ViewController.h"
#import <pthread.h>

@interface ViewController ()
@property (assign, nonatomic) pthread_rwlock_t lock; //讀寫鎖
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化鎖
    pthread_rwlock_init(&_lock, NULL);
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    for (int i = 0; i < 10; i++) {
        //讀
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_async(queue, ^{
            [self read];
        });
        //寫
        dispatch_async(queue, ^{
            [self write];
        });
        dispatch_async(queue, ^{
            [self write];
        });
        dispatch_async(queue, ^{
            [self write];
        });
    }
}


- (void)read {
    
    //讀-嘗試加鎖
    //pthread_rwlock_tryrdlock(&_lock);
    
    //讀-加鎖
    pthread_rwlock_rdlock(&_lock);
    
    sleep(1);
    NSLog(@"%s--線程:%@", __func__,[NSThread currentThread]);
    //解鎖
    pthread_rwlock_unlock(&_lock);
}

- (void)write
{
    //寫-嘗試加鎖
    //pthread_rwlock_trywrlock(&_lock);
    
    //寫-加鎖
    pthread_rwlock_wrlock(&_lock);
    
    sleep(1);
    NSLog(@"%s--線程:%@", __func__,[NSThread currentThread]);
    
    //解鎖
    pthread_rwlock_unlock(&_lock);
}

- (void)dealloc
{
    //銷毀
    pthread_rwlock_destroy(&_lock);
}
@end

打犹蟆:

2021-06-01 15:41:18.160200+0800 -[read]--線程:<NSThread: 0x600000356780>{number = 4, name = (null)}
2021-06-01 15:41:18.160212+0800 -[read]--線程:<NSThread: 0x600000374a40>{number = 7, name = (null)}
2021-06-01 15:41:19.162515+0800 -[write]--線程:<NSThread: 0x600000351c80>{number = 3, name = (null)}
2021-06-01 15:41:20.165942+0800 -[read]--線程:<NSThread: 0x600000341640>{number = 5, name = (null)}
2021-06-01 15:41:21.166782+0800 -[write]--線程:<NSThread: 0x600000371a40>{number = 8, name = (null)}
2021-06-01 15:41:22.172355+0800 -[write]--線程:<NSThread: 0x600000365d80>{number = 9, name = (null)}
2021-06-01 15:41:23.176932+0800 -[read]--線程:<NSThread: 0x600000365d80>{number = 10, name = (null)}
2021-06-01 15:41:23.176946+0800 -[read]--線程:<NSThread: 0x600000372e40>{number = 11, name = (null)}
2021-06-01 15:41:23.176946+0800 -[read]--線程:<NSThread: 0x600000378e00>{number = 12, name = (null)}
2021-06-01 15:41:24.182928+0800 -[write]--線程:<NSThread: 0x600000364700>{number = 13, name = (null)}
2021-06-01 15:41:25.188879+0800 -[write]--線程:<NSThread: 0x600000365fc0>{number = 14, name = (null)}
2021-06-01 15:41:26.189801+0800 -[write]--線程:<NSThread: 0x600000364b00>{number = 15, name = (null)}
2021-06-01 15:41:27.194003+0800 -[read]--線程:<NSThread: 0x600000378dc0>{number = 18, name = (null)}
2021-06-01 15:41:27.193997+0800 -[read]--線程:<NSThread: 0x600000365dc0>{number = 17, name = (null)}
2021-06-01 15:41:27.193997+0800 -[read]--線程:<NSThread: 0x600000374480>{number = 16, name = (null)}
2021-06-01 15:41:28.194641+0800 -[write]--線程:<NSThread: 0x600000372e40>{number = 19, name = (null)}
2021-06-01 15:41:29.198154+0800 -[write]--線程:<NSThread: 0x600000365a40>{number = 20, name = (null)}
2021-06-01 15:41:30.203043+0800 -[write]--線程:<NSThread: 0x6000003746c0>{number = 21, name = (null)}
2021-06-01 15:41:31.208880+0800 -[read]--線程:<NSThread: 0x600000365ec0>{number = 22, name = (null)}
2021-06-01 15:41:31.208880+0800 -[read]--線程:<NSThread: 0x600000374ac0>{number = 24, name = (null)}
2021-06-01 15:41:31.208874+0800 -[read]--線程:<NSThread: 0x600000372940>{number = 23, name = (null)}
2021-06-01 15:41:32.209671+0800 -[write]--線程:<NSThread: 0x600000374e80>{number = 25, name = (null)}
2021-06-01 15:41:33.215514+0800 -[write]--線程:<NSThread: 0x600000374940>{number = 26, name = (null)}
2021-06-01 15:41:34.221247+0800 -[write]--線程:<NSThread: 0x600000365c00>{number = 27, name = (null)}
2021-06-01 15:41:35.227011+0800 -[read]--線程:<NSThread: 0x600000372940>{number = 29, name = (null)}
2021-06-01 15:41:35.227010+0800 -[read]--線程:<NSThread: 0x6000003746c0>{number = 28, name = (null)}
2021-06-01 15:41:35.227011+0800 -[read]--線程:<NSThread: 0x600000378e40>{number = 30, name = (null)}
2021-06-01 15:41:36.231641+0800 -[write]--線程:<NSThread: 0x600000372e00>{number = 31, name = (null)}
2021-06-01 15:41:37.237418+0800 -[write]--線程:<NSThread: 0x600000365540>{number = 32, name = (null)}
2021-06-01 15:41:38.243212+0800 -[write]--線程:<NSThread: 0x600000372fc0>{number = 33, name = (null)}

可以發(fā)現(xiàn),讀同時進行刘离,寫就不能同時進行了室叉。

2. dispatch_barrier_async

廢話少說,先看怎么使用硫惕,如下:

#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) dispatch_queue_t queue;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //手動創(chuàng)建并發(fā)隊列
    self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i < 10; i++) {
        //讀
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_async(self.queue, ^{
            [self read];
        });
        //寫
        //當有一條線程在執(zhí)行這個任務(wù)的時候茧痕,絕不允許queue中有其他線程在執(zhí)行其他任務(wù)(包括上面的read和下面的write)
        dispatch_barrier_async(self.queue, ^{
            [self write];
        });
        dispatch_barrier_async(self.queue, ^{
            [self write];
        });
        dispatch_barrier_async(self.queue, ^{
            [self write];
        });
    }
}

- (void)read {
    sleep(1);
    NSLog(@"%s--線程:%@", __func__,[NSThread currentThread]);
}

- (void)write
{
    sleep(1);
    NSLog(@"%s--線程:%@", __func__,[NSThread currentThread]);
}
@end

打印如下:

2021-06-01 16:00:12.233462+0800 -[read]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:12.233472+0800 -[read]--線程:<NSThread: 0x60000144c240>{number = 3, name = (null)}
2021-06-01 16:00:12.233479+0800 -[read]--線程:<NSThread: 0x600001445d80>{number = 5, name = (null)}
2021-06-01 16:00:13.237569+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:14.239756+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:15.243263+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:16.248324+0800 -[read]--線程:<NSThread: 0x60000144c240>{number = 3, name = (null)}
2021-06-01 16:00:16.248324+0800 -[read]--線程:<NSThread: 0x600001445d80>{number = 5, name = (null)}
2021-06-01 16:00:16.248324+0800 -[read]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:17.253537+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:18.256256+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:19.257152+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:20.262030+0800 -[read]--線程:<NSThread: 0x600001445d80>{number = 5, name = (null)}
2021-06-01 16:00:20.262026+0800 -[read]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:20.262049+0800 -[read]--線程:<NSThread: 0x600001445b00>{number = 7, name = (null)}
2021-06-01 16:00:21.262937+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:22.268470+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:23.273291+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:24.279084+0800 -[read]--線程:<NSThread: 0x600001445b00>{number = 7, name = (null)}
2021-06-01 16:00:24.279085+0800 -[read]--線程:<NSThread: 0x600001445d80>{number = 5, name = (null)}
2021-06-01 16:00:24.279085+0800 -[read]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:25.281132+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:26.282865+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:27.286220+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:28.290012+0800 -[read]--線程:<NSThread: 0x600001445d80>{number = 5, name = (null)}
2021-06-01 16:00:28.290012+0800 -[read]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:28.290012+0800 -[read]--線程:<NSThread: 0x600001445b00>{number = 7, name = (null)}
2021-06-01 16:00:29.290888+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:30.296537+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}
2021-06-01 16:00:31.301259+0800 -[write]--線程:<NSThread: 0x60000147ab80>{number = 6, name = (null)}

可以發(fā)現(xiàn),打印結(jié)果和讀寫鎖一樣恼除,讀同時進行踪旷,寫就不能同時進行,說明dispatch_barrier_async在文件IO操作中也是有用的。

那么是如何做到的呢令野?

  1. 首先舀患,對于讀操作,使用dispatch_async气破,所以讀操作是異步的聊浅。
  2. 對于寫操作,使用dispatch_barrier_async堵幽,保證當有一條線程在執(zhí)行這個任務(wù)的時候狗超,絕不允許queue中有其他線程在執(zhí)行其他任務(wù)

示意圖如下:

柵欄.png

注意:這個dispatch_barrier_async函數(shù)傳入的并發(fā)隊列必須是自己手動通過dispatch_queue_cretate創(chuàng)建的朴下。如果傳入的是一個串行或是一個全局的并發(fā)隊列努咐,那這個函數(shù)便等同于dispatch_async函數(shù)的效果。

dispatch_barrier_async和dispatch_barrier_sync的區(qū)別

相同點:使用dispatch_barrier_async和dispatch_barrier_sync殴胧,都保證當有一條線程在執(zhí)行這個任務(wù)的時候渗稍,絕不允許queue中有其他線程在執(zhí)行其他任務(wù)。

不同點:dispatch_barrier_async是異步柵欄团滥,會開啟新線程(如上打印所示)竿屹,dispatch_barrier_sync是同步柵欄,不會開啟新線程灸姊。

如果把上面代碼改成:

dispatch_barrier_sync(self.queue, ^{
    [self write];
});

打印如下:

2021-06-01 16:20:51.992188+0800 -[read]--線程:<NSThread: 0x6000034107c0>{number = 4, name = (null)}
2021-06-01 16:20:51.992205+0800 -[read]--線程:<NSThread: 0x6000034121c0>{number = 5, name = (null)}
2021-06-01 16:20:51.992208+0800 -[read]--線程:<NSThread: 0x600003401cc0>{number = 6, name = (null)}
2021-06-01 16:20:52.993967+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:20:53.995608+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:20:54.996736+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:20:56.001371+0800 -[read]--線程:<NSThread: 0x6000034121c0>{number = 5, name = (null)}
2021-06-01 16:20:56.001470+0800 -[read]--線程:<NSThread: 0x600003401cc0>{number = 6, name = (null)}
2021-06-01 16:20:56.001486+0800 -[read]--線程:<NSThread: 0x6000034107c0>{number = 4, name = (null)}
2021-06-01 16:20:57.002200+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:20:58.003470+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:20:59.004165+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:00.005287+0800 -[read]--線程:<NSThread: 0x600003401cc0>{number = 6, name = (null)}
2021-06-01 16:21:00.005287+0800 -[read]--線程:<NSThread: 0x6000034107c0>{number = 4, name = (null)}
2021-06-01 16:21:00.005287+0800 -[read]--線程:<NSThread: 0x6000034121c0>{number = 5, name = (null)}
2021-06-01 16:21:01.005890+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:02.006747+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:03.008123+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:04.009767+0800 -[read]--線程:<NSThread: 0x6000034121c0>{number = 5, name = (null)}
2021-06-01 16:21:04.009768+0800 -[read]--線程:<NSThread: 0x600003401cc0>{number = 6, name = (null)}
2021-06-01 16:21:04.009781+0800 -[read]--線程:<NSThread: 0x60000341fac0>{number = 3, name = (null)}
2021-06-01 16:21:05.010196+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:06.010984+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:07.011930+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:08.014189+0800 -[read]--線程:<NSThread: 0x6000034121c0>{number = 5, name = (null)}
2021-06-01 16:21:08.014189+0800 -[read]--線程:<NSThread: 0x600003401cc0>{number = 6, name = (null)}
2021-06-01 16:21:08.014190+0800 -[read]--線程:<NSThread: 0x6000034107c0>{number = 4, name = (null)}
2021-06-01 16:21:09.015798+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:10.016433+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}
2021-06-01 16:21:11.017820+0800 -[write]--線程:<NSThread: 0x600003454980>{number = 1, name = main}

可以發(fā)現(xiàn)拱燃,使用dispatch_barrier_sync沒有開啟新線程,寫操作在當前線程(主線程)執(zhí)行力惯。

Demo地址:加鎖方案2

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碗誉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子父晶,更是在濱河造成了極大的恐慌哮缺,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甲喝,死亡現(xiàn)場離奇詭異尝苇,居然都是意外死亡,警方通過查閱死者的電腦和手機埠胖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門糠溜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人押袍,你說我怎么就攤上這事诵冒。” “怎么了谊惭?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵汽馋,是天一觀的道長侮东。 經(jīng)常有香客問我,道長豹芯,這世上最難降的妖魔是什么悄雅? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮铁蹈,結(jié)果婚禮上宽闲,老公的妹妹穿的比我還像新娘。我一直安慰自己握牧,他們只是感情好容诬,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沿腰,像睡著了一般览徒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上颂龙,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天习蓬,我揣著相機與錄音,去河邊找鬼措嵌。 笑死躲叼,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的企巢。 我是一名探鬼主播枫慷,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浪规!你這毒婦竟也來了流礁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤罗丰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后再姑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萌抵,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年元镀,在試婚紗的時候發(fā)現(xiàn)自己被綠了绍填。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡栖疑,死狀恐怖讨永,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情遇革,我是刑警寧澤卿闹,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布揭糕,位于F島的核電站,受9級特大地震影響锻霎,放射性物質(zhì)發(fā)生泄漏著角。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一旋恼、第九天 我趴在偏房一處隱蔽的房頂上張望吏口。 院中可真熱鬧,春花似錦冰更、人聲如沸产徊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舟铜。三九已至,卻和暖如春审葬,著一層夾襖步出監(jiān)牢的瞬間深滚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工涣觉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留痴荐,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓官册,卻偏偏與公主長得像生兆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子膝宁,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容