iOS-多線程02-多線程安全隱患及解決方案(常用鎖)

上一篇文章,我們大致了解了,多線程的基本知識(shí)和GCD的基本使用,這篇文章,我們主要來(lái)看看多線程的隱患和解決方案。
一般在我們項(xiàng)目中,可能會(huì)有這種情況:一塊資源可能會(huì)被多個(gè)線程共享,也就是多個(gè)線程可能會(huì)訪問(wèn)同一塊資源;或者說(shuō)是多個(gè)線程訪問(wèn)同一個(gè)對(duì)象、變量、文件等等。這時(shí)候,如果不采取一定的措施,很容易引發(fā)數(shù)據(jù)錯(cuò)亂和數(shù)據(jù)安全的問(wèn)題。就像我們以前很早就聽(tīng)說(shuō)過(guò)的賣票的例子,如果在賣票過(guò)程中不去做相應(yīng)的限制,這樣如果幾個(gè)窗口同一時(shí)間賣票,這樣在原來(lái)的票數(shù)的基礎(chǔ)上扣減的票數(shù)就會(huì)出現(xiàn)問(wèn)題,就好像下面這張圖:


image.png

所以這個(gè)時(shí)候,我們需要去解決這個(gè)問(wèn)題。類似的例子還有像在銀行存錢取錢的操作。如果不做限制,同一時(shí)間存錢取錢,也必然會(huì)出現(xiàn)問(wèn)題,如下圖:


image.png

這個(gè)問(wèn)題的關(guān)鍵點(diǎn)就是,同一時(shí)間,做了不同的操作,每個(gè)操作對(duì)原來(lái)數(shù)據(jù)都產(chǎn)生了影響,幾個(gè)影響沒(méi)有依次進(jìn)行,導(dǎo)致出現(xiàn)數(shù)據(jù)錯(cuò)亂。
舉個(gè)例子,銀行賬目上原先有1000,在取錢500的同時(shí),又存錢500,這時(shí)候大家第一時(shí)間就是覺(jué)得,還剩余1000,這只是正常現(xiàn)象,如果存取操作同時(shí)進(jìn)行的話,因?yàn)榛A(chǔ)都是1000,所以,取完錢剩余500,存完錢剩余1500,這兩個(gè)數(shù)據(jù)任何一個(gè)放回賬目上都是不正確的。這就是多條線程同時(shí)操作一個(gè)對(duì)象所引發(fā)的問(wèn)題。
這個(gè),我們解決問(wèn)題的方案就是:使用線程同步技術(shù)(同步,就是協(xié)同步調(diào),按預(yù)定的先后次序進(jìn)行)。而在iOS領(lǐng)域中,常見(jiàn)的線程同步技術(shù)就是:加鎖。

先來(lái)看看我們iOS線程同步技術(shù)的一些方案,也就是我們接下來(lái)要分析的方案:

OSSpinLock
os_unfair_lock
pthread_mutex
dispatch_semaphore
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized

第一種OSSpinLock,如果了解這個(gè)鎖的同學(xué),應(yīng)該知道,這個(gè)方案已經(jīng)被蘋果廢棄,當(dāng)然,現(xiàn)在仍然可用,只是蘋果不推薦使用。不過(guò),我們還是要來(lái)看看這個(gè)鎖的使用:

首先,創(chuàng)建一個(gè)base對(duì)象BLBaseDemo,里面封裝了賣票和存取錢的操作,而且是多線程異步操作,基本代碼如下:
/**
 存錢、取錢演示
 */
- (void)moneyTest
{
    self.money = 100;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self __saveMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self __drawMoney];
        }
    });
}

/**
 存錢
 */
- (void)__saveMoney
{
    int oldMoney = self.money;
    sleep(.2);
    oldMoney += 50;
    self.money = oldMoney;
    
    NSLog(@"存50,還剩%d元 - %@", oldMoney, [NSThread currentThread]);
}

/**
 取錢
 */
- (void)__drawMoney
{
    int oldMoney = self.money;
    sleep(.2);
    oldMoney -= 20;
    self.money = oldMoney;
    
    NSLog(@"取20,還剩%d元 - %@", oldMoney, [NSThread currentThread]);
}

/**
 賣票演示
 */
- (void)ticketTest
{
    self.ticketsCount = 15;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self __saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self __saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self __saleTicket];
        }
    });
}

/**
 賣1張票
 */
- (void)__saleTicket
{
    int oldTicketsCount = self.ticketsCount;
    sleep(.2);
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    NSLog(@"還剩%d張票 - %@", oldTicketsCount, [NSThread currentThread]);
}
如果直接調(diào)用實(shí)例對(duì)象的方法,如下:
BLBaseDemo *demo = [[BLBaseDemo alloc] init];
[demo moneyTest];
[demo ticketTest];
在正常流程下,最后的結(jié)果是錢剩余400,票剩余0
但是實(shí)際打印最后的結(jié)果:
2019-09-05 10:56:59.051802+0800 002-多線程加鎖方案[60823:35399754] 存50,還剩350元 - <NSThread: 0x6000018dc0c0>{number = 3, name = (null)}
2019-09-05 11:12:48.618159+0800 002-多線程加鎖方案[61082:35418439] 還剩2張票 - <NSThread: 0x600000ae4d40>{number = 3, name = (null)}
可以看到,錢和票的結(jié)果都是錯(cuò)了,這就是多線程同事操作同一個(gè)對(duì)象或者方法造成的隱患。

接下來(lái),我們用OSSpinLock加鎖方法來(lái)解決問(wèn)題:

首先,我們創(chuàng)建一個(gè)子類BLOSSpinLockDemo,繼承BLBaseDemo,然后定義兩個(gè)鎖,票鎖和錢鎖,初始化對(duì)象的同時(shí)對(duì)它們進(jìn)行初始化。然后重寫賣票和存取錢的方法,在方法中加鎖解鎖:

@interface OSSpinLockDemo()
@property (assign, nonatomic) OSSpinLock moneyLock;
@property (assign, nonatomic) OSSpinLock ticketLock;
@end
- (instancetype)init
{
    if (self = [super init]) {
        self.moneyLock = OS_SPINLOCK_INIT;
        self.ticketLock = OS_SPINLOCK_INIT;
    }
    return self;
}

- (void)__drawMoney
{
    OSSpinLockLock(&_moneyLock);
    
    [super __drawMoney];
    
    OSSpinLockUnlock(&_moneyLock);
}

- (void)__saveMoney
{
    OSSpinLockLock(&_moneyLock);
    
    [super __saveMoney];
    
    OSSpinLockUnlock(&_moneyLock);
}

- (void)__saleTicket
{
    OSSpinLockLock(&_ticketLock);
    
    [super __saleTicket];
    
    OSSpinLockUnlock(&_ticketLock);
}
這樣我們調(diào)用實(shí)例對(duì)象的方法
BLBaseDemo *demo = [[BLOSSpinLockDemo alloc] init];
[demo moneyTest];
[demo ticketTest];
所打印結(jié)果如下:
2019-09-05 11:17:10.600189+0800 002-多線程加鎖方案[61172:35424465] 取20,還剩400元 - <NSThread: 0x600002b37280>{number = 7, name = (null)}
2019-09-05 11:17:10.597382+0800 002-多線程加鎖方案[61172:35424478] 還剩0張票 - <NSThread: 0x600002b32680>{number = 6, name = (null)}
這里票和錢的結(jié)果都是我們想要的,并且是正確的結(jié)果。

總結(jié):OSSpinLock是一個(gè)自旋鎖(后面再做介紹),通過(guò)加鎖解鎖,可以有效解決多線程隱患問(wèn)題。
OSSpinLock是第一個(gè)同步技術(shù)方案,接下來(lái)就是os_unfair_lock,在分析這個(gè)方案之前,我們先來(lái)了解下自旋鎖和互斥鎖。所謂的自旋鎖,即等待鎖的線程會(huì)處于忙等(busy-wait)狀態(tài),一直占用著CPU資源,直到鎖被打開(kāi)后,退出忙等,并且進(jìn)入加鎖狀態(tài)。
而互斥鎖,就是等待鎖的過(guò)程是處理休眠的過(guò)程,在這個(gè)過(guò)程中不會(huì)占用CPU資源,一旦鎖被打開(kāi)后,重新喚醒等待的線程,然后進(jìn)行加鎖。
從使用的情況來(lái)看,兩個(gè)鎖的主要區(qū)別在于,是否會(huì)占用CPU資源,當(dāng)時(shí)不是說(shuō)互斥鎖不占用CPU資源,就一定比自旋鎖好,因?yàn)樵趩拘堰^(guò)程中也是會(huì)消耗內(nèi)存的。
只是說(shuō)OSSpinLock這種自旋鎖會(huì)出現(xiàn)一種狀況,當(dāng)優(yōu)先級(jí)低的線程加鎖之后,優(yōu)先級(jí)高的線程在等待的過(guò)程中,可能出現(xiàn)優(yōu)先級(jí)高的線程會(huì)一直占著CPU資源,導(dǎo)致優(yōu)先級(jí)低的線程沒(méi)法釋放鎖,出現(xiàn)線程死鎖狀態(tài)。而互斥鎖雖然在喚醒線程的時(shí)候會(huì)消耗CPU,但是不會(huì)出現(xiàn)死鎖狀態(tài),相對(duì)比較安全,所以目前蘋果從iOS10開(kāi)始就推薦大家用os_unfair_lock來(lái)替換OSSpinLock。
接下來(lái),我們來(lái)看看os_unfair_lock的使用

@interface BLOSUnfairLockDemo ()

@property (nonatomic, assign) os_unfair_lock moneyLock;
@property (nonatomic, assign) os_unfair_lock ticketLock;

@end

@implementation BLOSUnfairLockDemo

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

- (void)__drawMoney {
    
    os_unfair_lock_lock(&_moneyLock);
    
    [super __drawMoney];
    
    os_unfair_lock_unlock(&_moneyLock);
}

- (void)__saveMoney {
    
    os_unfair_lock_lock(&_moneyLock);
    
    [super __saveMoney];
    
    os_unfair_lock_unlock(&_moneyLock);
}

- (void)__saleTicket {
    
    os_unfair_lock_lock(&_ticketLock);
    
    [super __saleTicket];
    
    os_unfair_lock_unlock(&_ticketLock);
}

@end
這樣我們調(diào)用實(shí)例對(duì)象的方法
BLBaseDemo *demo = [[BLOSUnfairLockDemo alloc] init];
[demo moneyTest];
[demo ticketTest];
所打印結(jié)果如下:
2019-09-05 15:36:21.394991+0800 002-多線程加鎖方案[65256:35655513] 取20,還剩400元 - <NSThread: 0x600001de4a00>{number = 7, name = (null)}
2019-09-05 15:36:21.392122+0800 002-多線程加鎖方案[65256:35655524] 還剩0張票 - <NSThread: 0x600001dc8f80>{number = 6, name = (null)}
所以os_unfair_lock可以提供線程同步技術(shù),而且目前來(lái)看,蘋果更希望我們使用它來(lái)替換OSSpinLock

接下來(lái),我們來(lái)看一個(gè)用C語(yǔ)言編寫的鎖,就是pthread_mutex,mutex也是一種互斥鎖。先看看基本的使用

@interface BLMutexDemo ()

@property (nonatomic, assign) pthread_mutex_t ticketMutex;
@property (nonatomic, assign) pthread_mutex_t moneyMutex;

@end

@implementation BLMutexDemo

- (void)initMutex:(pthread_mutex_t *)mutex {
    // 初始化屬性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
    
    // 初始化mutex,參數(shù)一是pthread_mutex_t(鎖) 參數(shù)二是pthread_mutexattr_t(屬性)
    pthread_mutex_init(mutex, &attr);
    //使用完屬性之后 要銷毀
    pthread_mutexattr_destroy(&attr);
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        [self initMutex:&_ticketMutex];
        [self initMutex:&_moneyMutex];
    }
    return self;
}

- (void)__drawMoney {
    
    pthread_mutex_lock(&_moneyMutex);
    
    [super __drawMoney];
    
    pthread_mutex_unlock(&_moneyMutex);
}

- (void)__saveMoney {
    
    pthread_mutex_lock(&_moneyMutex);
    
    [super __saveMoney];
    
    pthread_mutex_unlock(&_moneyMutex);
}

- (void)__saleTicket {
    
    pthread_mutex_lock(&_ticketMutex);
    
    [super __saleTicket];
    
    pthread_mutex_unlock(&_ticketMutex);
}

-(void)dealloc {
    
    pthread_mutex_destroy(&_ticketMutex);
    pthread_mutex_destroy(&_moneyMutex);
}

@end
這樣我們調(diào)用實(shí)例對(duì)象的方法
BLBaseDemo *demo = [[BLMutexDemo alloc] init];
[demo moneyTest];
[demo ticketTest];
所打印結(jié)果如下:
2019-09-05 16:24:30.859567+0800 002-多線程加鎖方案[66031:35707487] 存50,還剩400元 - <NSThread: 0x600001522180>{number = 3, name = (null)}
2019-09-05 16:24:30.857287+0800 002-多線程加鎖方案[66031:35707486] 還剩0張票 - <NSThread: 0x600001524ec0>{number = 4, name = (null)}

這是就pthread_mutex_t的基本使用,當(dāng)然在定義pthread_mutex_t時(shí),我們還可以用這種方式:

// 屬性參數(shù)傳NULL,默認(rèn)為PTHREAD_MUTEX_DEFAULT模式
pthread_mutex_init(mutex, NULL);

這里,還需要講一個(gè)知識(shí)點(diǎn)就是,屬性設(shè)置,pthread_mutexattr_t有幾種模式:

#define PTHREAD_MUTEX_NORMAL        0
#define PTHREAD_MUTEX_ERRORCHECK    1
#define PTHREAD_MUTEX_RECURSIVE     2
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL

這里我們?cè)賮?lái)看看PTHREAD_MUTEX_RECURSIVE這種模式,用這種模式定義的鎖,叫做遞歸鎖。顧名思義,這是在遞歸函數(shù)中使用的鎖。如下代碼:

@interface BLMutexDemo ()

@property (nonatomic, assign) pthread_mutex_t recursiveMutex;

@property (nonatomic, assign) int count;

@end

@implementation BLMutexDemo

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.count = 10;
        
        // 初始化屬性
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        
        // 初始化mutex,參數(shù)一是pthread_mutex_t(鎖) 參數(shù)二是pthread_mutexattr_t(屬性)
        pthread_mutex_init(&_recursiveMutex, &attr);
        //使用完屬性之后 要銷毀
        pthread_mutexattr_destroy(&attr);
        
    }
    return self;
}

- (void)otherTest {
    
    pthread_mutex_lock(&_recursiveMutex);
    
    
    if (self.count > 0) {
        self.count--;
        NSLog(@"count的值 -- %d",self.count);
        [self otherTest];
    }
    pthread_mutex_unlock(&_recursiveMutex);
}

-(void)dealloc {
    
    pthread_mutex_destroy(&_recursiveMutex);
}

@end
這樣我們調(diào)用實(shí)例對(duì)象的方法
BLBaseDemo *demo = [[BLMutexDemo alloc] init];
[demo otherTest];
所打印結(jié)果如下:
2019-09-05 16:38:41.741313+0800 002-多線程加鎖方案[66338:35728207] count的值 -- 9
2019-09-05 16:38:41.741421+0800 002-多線程加鎖方案[66338:35728207] count的值 -- 8
2019-09-05 16:38:41.741504+0800 002-多線程加鎖方案[66338:35728207] count的值 -- 7
2019-09-05 16:38:41.741587+0800 002-多線程加鎖方案[66338:35728207] count的值 -- 6
2019-09-05 16:38:41.741657+0800 002-多線程加鎖方案[66338:35728207] count的值 -- 5
2019-09-05 16:38:41.741724+0800 002-多線程加鎖方案[66338:35728207] count的值 -- 4
2019-09-05 16:38:41.741809+0800 002-多線程加鎖方案[66338:35728207] count的值 -- 3
2019-09-05 16:38:41.741891+0800 002-多線程加鎖方案[66338:35728207] count的值 -- 2
2019-09-05 16:38:41.741961+0800 002-多線程加鎖方案[66338:35728207] count的值 -- 1
2019-09-05 16:38:41.742033+0800 002-多線程加鎖方案[66338:35728207] count的值 -- 0

這就是pthread_mutex_t在PTHREAD_MUTEX_RECURSIVE模式下的使用,不過(guò)使用遞歸模式,要注意需要再同一個(gè)線程下使用,才能達(dá)到遞歸加鎖解鎖的效果。
除了PTHREAD_MUTEX_RECURSIVE遞歸模式,這里我們還需要知道另外一個(gè)知識(shí)點(diǎn),那就是條件(pthread_cond_t)。到這里大家可能覺(jué)得知識(shí)點(diǎn)有點(diǎn)多,因?yàn)楫吘惯€有好幾個(gè)鎖還沒(méi)有講到,其實(shí)后面的那些鎖,很多都是基于pthread_mutex_t這個(gè)鎖的基礎(chǔ)上來(lái)封裝的,了解了pthread_mutex_t,也就意味著知道后面的鎖的原理。那我們來(lái)看看什么是pthread_cond_t,很簡(jiǎn)單,條件條件,就是到達(dá)一種條件的情況下進(jìn)行操作,來(lái)看看代碼

@interface BLMutexDemo1 ()

@property (assign, nonatomic) pthread_mutex_t mutex;
@property (assign, nonatomic) pthread_cond_t cond;
@property (strong, nonatomic) NSMutableArray *data;

@end

@implementation BLMutexDemo1

- (instancetype)init
{
    if (self = [super init]) {
        // 初始化屬性
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
        // 初始化鎖
        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)者模式

// 線程1
// 刪除數(shù)組中的元素
- (void)__remove
{
    pthread_mutex_lock(&_mutex);
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
        // 等待
        pthread_cond_wait(&_cond, &_mutex);
    }
    
    [self.data removeLastObject];
    NSLog(@"刪除了元素");
    
    pthread_mutex_unlock(&_mutex);
}

// 線程2
// 往數(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);
}

@end
代碼大概的主要功能就是,一個(gè)數(shù)組的添加和刪除元素,只是在刪除元素的時(shí)候,我們需要保證數(shù)組先有元素,如果沒(méi)有元素的話,先去添加元素,這時(shí)就可以用pthread_cond_t,先在判斷數(shù)組元素個(gè)數(shù)為0的時(shí)候,使用pthread_cond_wait(),暫時(shí)放開(kāi)鎖,等待通知,然后再添加元素之后,使用pthread_cond_singal()來(lái)通知pthread_cond_wait()函數(shù)中的等待,這時(shí)候pthread_cond_wait()會(huì)重新加鎖,繼續(xù)原先刪除元素的操作,得到結(jié)果。
打印如下:
2019-09-05 16:56:09.054302+0800 002-多線程加鎖方案[66701:35751970] __remove - begin
2019-09-05 16:56:10.059322+0800 002-多線程加鎖方案[66701:35751971] 添加了元素
2019-09-05 16:56:10.059662+0800 002-多線程加鎖方案[66701:35751970] 刪除了元素

接來(lái)下就是GCD中的信號(hào)量dispatch_semaphore,這也是同步技術(shù)方案中,比較方便的一直方案。這里就簡(jiǎn)單舉個(gè)例子

@interface BLSemaphoreDemo ()

@property (nonatomic, strong) dispatch_semaphore_t semaphore;

@end

@implementation BLSemaphoreDemo

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.semaphore = dispatch_semaphore_create(3);
    }
    return self;
}

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

- (void)test {
    
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(3);
    
    NSLog(@"test --- %@", [NSThread currentThread]);
    
    dispatch_semaphore_signal(self.semaphore);
}

@end
這樣我們調(diào)用實(shí)例對(duì)象的方法
BLBaseDemo *demo = [[BLSemaphoreDemo alloc] init];
[demo otherTest];
所打印結(jié)果如下:
2019-09-05 17:07:58.970138+0800 002-多線程加鎖方案[67225:35770816] test --- <NSThread: 0x60000007b600>{number = 3, name = (null)}
2019-09-05 17:07:58.970183+0800 002-多線程加鎖方案[67225:35770825] test --- <NSThread: 0x60000007bc40>{number = 12, name = (null)}
2019-09-05 17:07:58.970157+0800 002-多線程加鎖方案[67225:35770824] test --- <NSThread: 0x60000007bac0>{number = 11, name = (null)}
2019-09-05 17:08:01.974692+0800 002-多線程加鎖方案[67225:35770821] test --- <NSThread: 0x60000007bf00>{number = 8, name = (null)}
2019-09-05 17:08:01.974707+0800 002-多線程加鎖方案[67225:35770819] test --- <NSThread: 0x60000007b6c0>{number = 6, name = (null)}
2019-09-05 17:08:01.974693+0800 002-多線程加鎖方案[67225:35770820] test --- <NSThread: 0x60000007b540>{number = 7, name = (null)}
2019-09-05 17:08:04.978897+0800 002-多線程加鎖方案[67225:35770823] test --- <NSThread: 0x60000007b940>{number = 10, name = (null)}
2019-09-05 17:08:04.978897+0800 002-多線程加鎖方案[67225:35770822] test --- <NSThread: 0x60000007b640>{number = 9, name = (null)}
2019-09-05 17:08:04.978897+0800 002-多線程加鎖方案[67225:35770817] test --- <NSThread: 0x60000007bcc0>{number = 4, name = (null)}
2019-09-05 17:08:07.980190+0800 002-多線程加鎖方案[67225:35770818] test --- <NSThread: 0x60000007be40>{number = 5, name = (null)}

總結(jié):在打印的結(jié)果中,我們可以看到,每三秒,打印一次,總共9條線程,這就是信號(hào)量的作用。他可以控制同時(shí)并發(fā)的最大線程數(shù)量,每一條線程到達(dá)dispatch_semaphore_wait()這里時(shí),會(huì)去判斷semaphore剩余可以使用的線程數(shù),因?yàn)槲覀円婚_(kāi)始初始化的時(shí)候定義是3條:self.semaphore = dispatch_semaphore_create(3),所以這里最大并發(fā)數(shù)就是3,這就是為什么每次打印是三條的原因,dispatch_semaphore_signal()這個(gè)函數(shù)是在當(dāng)前線程走完時(shí),恢復(fù)一條線程的使用,因?yàn)槲覀僼est函數(shù)中,用了sleep(3),所以每條線程都會(huì)卡3秒,這只是為了演示效果。
接下來(lái)就是NSLock、NSCondition和NSConditionLock的使用,NSLock其實(shí)就是針對(duì)pthread_mutex_t在默認(rèn)模式下的封裝使用,大致就是:

- (void)__saleTicket
{
    [self.ticketLock lock];
    
    [super __saleTicket];
    
    [self.ticketLock unlock];
}

而NSCondition,其實(shí)就是對(duì)pthread_cond_t加pthread_mutex_t的封裝使用,如下代碼:

@interface BLNSConditionDemo ()

@property (nonatomic, strong) NSCondition *con;
@property (strong, nonatomic) NSMutableArray *data;

@end

@implementation BLNSConditionDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.con = [[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];
}

- (void)__remove
{
    
    [self.con lock];
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
        [self.con wait];
    }
    
    [self.data removeLastObject];
    NSLog(@"刪除了元素");
    
    [self.con unlock];
}

- (void)__add
{
    
    [self.con lock];
    
    sleep(1);
    
    [self.data addObject:@"test"];
    NSLog(@"添加了元素");
    
    [self.con unlock];
    
    [self.con signal];
}

@end

而NSConditionLock,則是對(duì)NSCondition的進(jìn)一步封裝,如下

@interface BLNSConditionLockDemo ()

@property (nonatomic, strong) NSConditionLock *con;
@property (strong, nonatomic) NSMutableArray *data;

@end

@implementation BLNSConditionLockDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.con = [[NSConditionLock alloc] initWithCondition:1];
        self.data = [NSMutableArray array];
    }
    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];
}

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

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

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

@end

NSConditionLock主要作用就在于,它可以在滿足一個(gè)條件的情況下,進(jìn)行加鎖和解鎖。
最后還有兩種同步加鎖的方案,一種是dispatch_queue(DISPATCH_QUEUE_SERIAL),還有一種是@synchronized。
對(duì)于dispatch_queue(DISPATCH_QUEUE_SERIAL)大家都知道,因?yàn)榇嘘?duì)列,不管你是同步還是異步,它都是依次執(zhí)行任務(wù)的,所以可以達(dá)到加鎖效果。
而對(duì)于@synchronized,可能大家在日常的項(xiàng)目中已經(jīng)有所用到,大概就是@synchronized(self),其實(shí),在()中,只要是同一個(gè)對(duì)象,就可以達(dá)到加鎖的效果,這里我就不做過(guò)多說(shuō)明了,但是這個(gè)加鎖方式性能上不是很好,因?yàn)檫@簡(jiǎn)單一句話,封裝了很多內(nèi)容。這里,既然講到性能,我們也可以進(jìn)行一定的排序,如下圖:


image.png

其實(shí)也蠻容易理解的,首先是蘋果在iOS中主推的os_unfair_lock,其次就是被它替代的OSSpinLock,然后是GCD的信號(hào)量,接著是C語(yǔ)言的pthread_mutex,然后是串行隊(duì)列,后面NS打頭的Foundation框架的對(duì)象,都是基于pthread_mutex_t進(jìn)行封裝的,自然往后排。最后就是@synchronized。
這里,推薦大家平常使用的,有信號(hào)量和pthread_mutex,因?yàn)閛s_unfair_lock是iOS10之后才出來(lái)的,而OSSpinLock又不推薦使用,所以性能又高,又比較兼容的就是信號(hào)量和和pthread_mutex。
iOS的多線程隱患和解決方案的幾大鎖,就先分享到這里,后續(xù)可能會(huì)有一些補(bǔ)充,希望對(duì)大家有所幫助!

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

  • 鎖是一種同步機(jī)制,用于多線程環(huán)境中對(duì)資源訪問(wèn)的限制iOS中常見(jiàn)鎖的性能對(duì)比圖(摘自:ibireme): iOS鎖的...
    LiLS閱讀 1,630評(píng)論 0 6
  • 一. 多線程 1.常見(jiàn)多線程方案 pthread : 純粹 C 語(yǔ)言的API,跨平臺(tái), 線程生命周期程序員管理...
    天明天閱讀 4,279評(píng)論 0 14
  • 一、簡(jiǎn)介:多線程在之前進(jìn)行過(guò)一篇詳細(xì)的基礎(chǔ)博客 iOS多線程 二、多線程的基礎(chǔ)知識(shí)回顧 1.1、iOS中的常見(jiàn)多線...
    IIronMan閱讀 1,028評(píng)論 0 4
  • 前言 一塊資源可能會(huì)被多個(gè)線程共享,也就是多個(gè)線程可能會(huì)訪問(wèn)同一塊資源,比如多個(gè)線程訪問(wèn)同一個(gè)對(duì)象、同一個(gè)變量、同...
    WQ_UESTC閱讀 998評(píng)論 0 5
  • 一、前言 前段時(shí)間看了幾個(gè)開(kāi)源項(xiàng)目,發(fā)現(xiàn)他們保持線程同步的方式各不相同,有@synchronized、NSLock...
    稻春閱讀 504評(píng)論 0 0

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