iOS多線程之線程鎖

在多線程開發中,不可忽視的一個問題就是多個線程同時訪問同一個資源時,會造成髒數據等預想不到的結果,爲了避免這種現象,我們需要在訪問資源的時候添加線程鎖,來控制訪問。

添加線程鎖的方式在這主要講三種方式:

  • @synchronized隱式創建鎖對象
  • NSLock
  • GCD的dispatch_semaphore_t信號機制
一、@synchronized( )

@synchronized()內的對象設定爲鎖的唯一標識,只有標識相同時,才滿足互斥

-(void)testSynchronized
{
    GCDcreate *someone = [GCDcreate new];
    //線程A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (someone) {
            NSLog(@"線程A=%@",[NSThread currentThread]);
            someone.name = @"我是線程A";
            [NSThread sleepForTimeInterval:5];
        }
    });
    //線程B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (someone) {
            NSLog(@"線程B=%@",[NSThread currentThread]);
            someone.name = @"我是線程B";
        }
    });
}
二、NSLock

NSLock的lock和unlock必須在同一線程;同一線程lock後,未unlock前再lock會導致永久性死鎖。

-(void)testNSLock
{
    GCDcreate *someone = [GCDcreate new];
    //創建鎖對象
    NSLock *alock = [[NSLock alloc] init];
    //線程A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        if ([alock tryLock]) //上鎖 或 [alock lock]
        {
            NSLog(@"線程A=%@",[NSThread currentThread]);
            someone.name = @"我是線程A";
            [NSThread sleepForTimeInterval:5];
            //開鎖
            [alock unlock];
        }
    });
    //線程B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        if ([alock tryLock]) //上鎖 或 [alock lock]
        {
            NSLog(@"線程B=%@",[NSThread currentThread]);
            someone.name = @"我是線程B";
            //開鎖
            [alock unlock];
        }
    });
}
三、dispatch_semaphore_t

在上一篇GCD用法詳解中講解到dispatch_semaphore_t信號量的用法,它的用法很靈活,其中一種就是可以當成線程鎖來用。

這裏拿一個現實中的例子來解釋一下它的用法,再買火車的時候,會有很多窗口或代售點出售火車票,那麼火車票的餘票數是保存在數據庫中的,每個出售的地方都會去讀寫這個餘票數。如果不加鎖的話,就會出現多個窗口買同一張票的情況。比如:

//初始化火車票數量、賣票窗口、並開始賣票
- (void)testGCD {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"semaphore---begin");
    
    self.ticketSurplusCount = 50;
    // queue1 代表北京火車票售賣窗口
    dispatch_queue_t queue1 = dispatch_queue_create("com.jzsec.GCDtest", DISPATCH_QUEUE_CONCURRENT);
    //queue2 代表上海火車票售賣窗口
    dispatch_queue_t queue2 = dispatch_queue_create("com.jzsec.GCDtest1", DISPATCH_QUEUE_CONCURRENT);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketSafe];
    });
}

//售賣火車票
- (void)saleTicketSafe {
    while (1) {
        if (self.ticketSurplusCount > 0) {  //如果還有票,繼續售賣
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩餘票數:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { //如果已賣完,關閉售票窗口
            NSLog(@"所有火車票均已售完");
            break;
        }
    }   
}

結果比較長就不全貼出來了,其中一段就可以看出問題:

...
2019-08-09 18:36:32.036600+0800 GCDtest[2616:300126] 剩餘票數:40 窗口:<NSThread: 0x600001b8c080>{number = 3, name = (null)}
2019-08-09 18:36:32.036600+0800 GCDtest[2616:300123] 剩餘票數:39 窗口:<NSThread: 0x600001b87f80>{number = 4, name = (null)}
2019-08-09 18:36:32.237564+0800 GCDtest[2616:300123] 剩餘票數:38 窗口:<NSThread: 0x600001b87f80>{number = 4, name = (null)}
2019-08-09 18:36:32.237565+0800 GCDtest[2616:300126] 剩餘票數:38 窗口:<NSThread: 0x600001b8c080>{number = 3, name = (null)}
2019-08-09 18:36:32.442719+0800 GCDtest[2616:300123] 剩餘票數:37 窗口:<NSThread: 0x600001b87f80>{number = 4, name = (null)}
2019-08-09 18:36:32.442719+0800 GCDtest[2616:300126] 剩餘票數:37 窗口:<NSThread: 0x600001b8c080>{number = 3, name = (null)}
2019-08-09 18:36:32.644508+0800 GCDtest[2616:300123] 剩餘票數:36 窗口:<NSThread: 0x600001b87f80>{number = 4, name = (null)}
2019-08-09 18:36:32.644508+0800 GCDtest[2616:300126] 剩餘票數:35 窗口:<NSThread: 0x600001b8c080>{number = 3, name = (null)}
...

這裏出現兩個37和兩個38,就是因爲兩個線程同時讀寫self.ticketSurplusCount餘票數造成的,如果線程更多的話,後果不堪設想。

所以我們對程序加上線程鎖,改造如下:

/**
 * 線程安全:使用 semaphore 加鎖
 * 初始化火車票數量、賣票窗口(線程安全)、並開始賣票
 */
- (void)testGCD {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"semaphore---begin");
    
    semaphoreLock = dispatch_semaphore_create(1);

    self.ticketSurplusCount = 50;    
    // queue1 代表北京火車票售賣窗口
    dispatch_queue_t queue1 = dispatch_queue_create("com.jzsec.GCDtest", DISPATCH_QUEUE_CONCURRENT);
    //queue2 代表上海火車票售賣窗口
    dispatch_queue_t queue2 = dispatch_queue_create("com.jzsec.GCDtest1", DISPATCH_QUEUE_CONCURRENT);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketSafe];
    });
}

/**
 * 售賣火車票(線程安全)
 */
- (void)saleTicketSafe {
    while (1) {
        // 相當於加鎖
        dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
        
        if (self.ticketSurplusCount > 0) {  //如果還有票,繼續售賣
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩餘票數:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { //如果已賣完,關閉售票窗口
            NSLog(@"所有火車票均已售完");   
            // 相當於解鎖
            dispatch_semaphore_signal(semaphoreLock);
            break;
        }   
        // 相當於解鎖
        dispatch_semaphore_signal(semaphoreLock);
    }   
}
2019-08-09 18:41:29.843021+0800 GCDtest[2660:303158] currentThread---<NSThread: 0x600002f86a00>{number = 1, name = main}
2019-08-09 18:41:29.843230+0800 GCDtest[2660:303158] semaphore---begin
2019-08-09 18:41:29.844289+0800 GCDtest[2660:303249] 剩餘票數:49 窗口:<NSThread: 0x600002fd6440>{number = 3, name = (null)}
2019-08-09 18:41:30.048023+0800 GCDtest[2660:303248] 剩餘票數:48 窗口:<NSThread: 0x600002fd7400>{number = 4, name = (null)}
2019-08-09 18:41:30.252102+0800 GCDtest[2660:303249] 剩餘票數:47 窗口:<NSThread: 0x600002fd6440>{number = 3, name = (null)}
2019-08-09 18:41:30.453306+0800 GCDtest[2660:303248] 剩餘票數:46 窗口:<NSThread: 0x600002fd7400>{number = 4, name = (null)}
2019-08-09 18:41:30.657614+0800 GCDtest[2660:303249] 剩餘票數:45 窗口:<NSThread: 0x600002fd6440>{number = 3, name = (null)}
... 省略 ...
2019-08-09 18:41:39.179921+0800 GCDtest[2660:303249] 剩餘票數:3 窗口:<NSThread: 0x600002fd6440>{number = 3, name = (null)}
2019-08-09 18:41:39.381520+0800 GCDtest[2660:303248] 剩餘票數:2 窗口:<NSThread: 0x600002fd7400>{number = 4, name = (null)}
2019-08-09 18:41:39.583975+0800 GCDtest[2660:303249] 剩餘票數:1 窗口:<NSThread: 0x600002fd6440>{number = 3, name = (null)}
2019-08-09 18:41:39.786290+0800 GCDtest[2660:303248] 剩餘票數:0 窗口:<NSThread: 0x600002fd7400>{number = 4, name = (null)}
2019-08-09 18:41:39.988175+0800 GCDtest[2660:303249] 所有火車票均已售完
2019-08-09 18:41:39.988514+0800 GCDtest[2660:303248] 所有火車票均已售完

此時就可以看到與票數是依次正確減少的,這種訪問數據纔是線程安全的。

初始semaphoreLock爲1,第一個線程進入saleTicketSafe時dispatch_semaphore_wait使semaphoreLock-1爲0,第二個線程再進入saleTicketSafe時dispatch_semaphore_wait使semaphoreLock-1爲-1,第二個線程就會一直等待,等第一個線程扣除餘票後dispatch_semaphore_signal使semaphoreLock+1爲0,此時正在等待中的第二個線程開始執行;

以此類推,總是同時只有一個線程在訪問它們的共享資源self.ticketSurplusCount,保證了self.ticketSurplusCount的安全性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章