iOS GCD (四) dispatch_semaphore 訊號量

weixin_33670713發表於2018-04-27

iOS GCD (一) 任務+佇列 基礎組合
iOS GCD (二 ) dispatch_group 佇列組
iOS GCD(三) dispatch_barrier_async 柵欄方法
iOS GCD (四) dispatch_semaphore 訊號量
iOS GCD(五) 死鎖案例分析
iOS GCD(六)執行緒加鎖

GCD 中的訊號量是指 Dispatch Semaphore,是持有計數的訊號。類似於過高速路收費站的欄杆。可以通過時,開啟欄杆,不可以通過時,關閉欄杆。在 Dispatch Semaphore 中,使用計數來完成這個功能,計數為0時等待,不可通過。計數為1或大於1時,計數減1且不等待,可通過。
Dispatch Semaphore 提供了三個函式。

1.dispatch_semaphore_create:建立一個Semaphore並初始化訊號的總量
2.dispatch_semaphore_signal:傳送一個訊號,讓訊號總量加1
3.dispatch_semaphore_wait:可以使總訊號量減1,當訊號總量為0時就會一直等待(阻塞所線上程),否則就可以正常執行。

注意:訊號量的使用前提是:想清楚你需要處理哪個執行緒等待(阻塞),又要哪個執行緒繼續執行,然後使用訊號量。

Dispatch Semaphore 在實際開發中主要用於:

保持執行緒同步,將非同步執行任務轉換為同步執行任務
保證執行緒安全,為執行緒加鎖

Dispatch Semaphore 執行緒同步

我們在開發中,會遇到這樣的需求:非同步執行耗時任務,並使用非同步執行的結果進行一些額外的操作。換句話說,相當於,將將非同步執行任務轉換為同步執行任務。比如說:AFNetworking 中 AFURLSessionManager.m 裡面的 tasksForKeyPath: 方法。通過引入訊號量的方式,等待非同步執行任務結果,獲取到 tasks,然後再返回該 tasks。

- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }
        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return tasks;
}

下面,我們來利用 Dispatch Semaphore 實現執行緒同步,將非同步執行任務轉換為同步執行任務。

/**
 * semaphore 執行緒同步
 */
- (void)semaphoreSync {
    
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 列印當前執行緒
    NSLog(@"semaphore---begin");
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任務1
        [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 列印當前執行緒
        
        number = 100;
        
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %zd",number);
}

輸出結果:
2018-02-23 22:22:26.521665+0800 YSC-GCD-demo[20642:5246341] currentThread---<NSThread: 0x60400006bc80>{number = 1, name = main}
2018-02-23 22:22:26.521869+0800 YSC-GCD-demo[20642:5246341] semaphore---begin
2018-02-23 22:22:28.526841+0800 YSC-GCD-demo[20642:5246638] 1---<NSThread: 0x600000272300>{number = 3, name = (null)}
2018-02-23 22:22:28.527030+0800 YSC-GCD-demo[20642:5246341] semaphore---end,number = 100

從 Dispatch Semaphore 實現執行緒同步的程式碼可以看到:

semaphore---end 是在執行完 number = 100; 之後才列印的。而且輸出結果 number 為 100。
這是因為非同步執行不會做任何等待,可以繼續執行任務。非同步執行將任務1追加到佇列之後,不做等待,接著執行dispatch_semaphore_wait方法。此時 semaphore == 0,當前執行緒進入等待狀態。然後,非同步任務1開始執行。任務1執行到dispatch_semaphore_signal之後,總訊號量,此時 semaphore == 1,dispatch_semaphore_wait方法使總訊號量減1,正在被阻塞的執行緒(主執行緒)恢復繼續執行。最後列印semaphore---end,number = 100。這樣就實現了執行緒同步,將非同步執行任務轉換為同步執行任務。

Dispatch Semaphore 執行緒安全和執行緒同步(為執行緒加鎖)

執行緒安全:如果你的程式碼所在的程式中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。
若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒安全的;若有多個執行緒同時執行寫操作(更改變數),一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。
執行緒同步:可理解為執行緒 A 和 執行緒 B 一塊配合,A 執行到一定程度時要依靠執行緒 B 的某個結果,於是停下來,示意 B 執行;B 依言執行,再將結果給 A;A 再繼續操作。
舉個簡單例子就是:兩個人在一起聊天。兩個人不能同時說話,避免聽不清(操作衝突)。等一個人說完(一個執行緒結束操作),另一個再說(另一個執行緒再開始操作)。
下面,我們模擬火車票售賣的方式,實現 NSThread 執行緒安全和解決執行緒同步問題。
場景:總共有50張火車票,有兩個售賣火車票的視窗,一個是北京火車票售賣視窗,另一個是上海火車票售賣視窗。兩個視窗同時售賣火車票,賣完為止。

1.非執行緒安全(不使用 semaphore)

先來看看不考慮執行緒安全的程式碼

/**
 * 非執行緒安全:不使用 semaphore
 * 初始化火車票數量、賣票視窗(非執行緒安全)、並開始賣票
 */
- (void)initTicketStatusNotSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 列印當前執行緒
    NSLog(@"semaphore---begin");
    
    self.ticketSurplusCount = 50;
    
    // queue1 代表北京火車票售賣視窗
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火車票售賣視窗
    dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketNotSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketNotSafe];
    });
}

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

輸出結果(部分):
2018-02-23 22:25:35.789072+0800 YSC-GCD-demo[20712:5258914] currentThread---<NSThread: 0x604000068880>{number = 1, name = main}
2018-02-23 22:25:35.789260+0800 YSC-GCD-demo[20712:5258914] semaphore---begin
2018-02-23 22:25:35.789641+0800 YSC-GCD-demo[20712:5259176] 剩餘票數:48 視窗:<NSThread: 0x60000027db80>{number = 3, name = (null)}
2018-02-23 22:25:35.789646+0800 YSC-GCD-demo[20712:5259175] 剩餘票數:49 視窗:<NSThread: 0x60000027e740>{number = 4, name = (null)}
2018-02-23 22:25:35.994113+0800 YSC-GCD-demo[20712:5259175] 剩餘票數:47 視窗:<NSThread: 0x60000027e740>{number = 4, name = (null)}
2018-02-23 22:25:35.994129+0800 YSC-GCD-demo[20712:5259176] 剩餘票數:46 視窗:<NSThread: 0x60000027db80>{number = 3, name = (null)}
2018-02-23 22:25:36.198993+0800 YSC-GCD-demo[20712:5259176] 剩餘票數:45 視窗:<NSThread: 0x60000027db80>{number = 3, name = (null)}

可以看到在不考慮執行緒安全,不使用 semaphore 的情況下,得到票數是錯亂的,這樣顯然不符合我們的需求,所以我們需要考慮執行緒安全問題。

2. 執行緒安全(使用 semaphore 加鎖)

考慮執行緒安全的程式碼:

/**
 * 執行緒安全:使用 semaphore 加鎖
 * 初始化火車票數量、賣票視窗(執行緒安全)、並開始賣票
 */
- (void)initTicketStatusSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 列印當前執行緒
    NSLog(@"semaphore---begin");
    
    semaphoreLock = dispatch_semaphore_create(1);
    
    self.ticketSurplusCount = 50;
    
    // queue1 代表北京火車票售賣視窗
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火車票售賣視窗
    dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __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);
    }
}

輸出結果為:
2018-02-23 22:32:19.814232+0800 YSC-GCD-demo[20862:5290531] currentThread---<NSThread: 0x6000000783c0>{number = 1, name = main}
2018-02-23 22:32:19.814412+0800 YSC-GCD-demo[20862:5290531] semaphore---begin
2018-02-23 22:32:19.814837+0800 YSC-GCD-demo[20862:5290687] 剩餘票數:49 視窗:<NSThread: 0x6040002709c0>{number = 3, name = (null)}
2018-02-23 22:32:20.017745+0800 YSC-GCD-demo[20862:5290689] 剩餘票數:48 視窗:<NSThread: 0x60000046c640>{number = 4, name = (null)}
2018-02-23 22:32:20.222039+0800 YSC-GCD-demo[20862:5290687] 剩餘票數:47 視窗:<NSThread: 0x6040002709c0>{number = 3, name = (null)}
......
2018-02-23 22:32:29.024817+0800 YSC-GCD-demo[20862:5290689] 剩餘票數:4 視窗:<NSThread: 0x60000046c640>{number = 4, name = (null)}
2018-02-23 22:32:29.230110+0800 YSC-GCD-demo[20862:5290687] 剩餘票數:3 視窗:<NSThread: 0x6040002709c0>{number = 3, name = (null)}
2018-02-23 22:32:29.433615+0800 YSC-GCD-demo[20862:5290689] 剩餘票數:2 視窗:<NSThread: 0x60000046c640>{number = 4, name = (null)}
2018-02-23 22:32:29.637572+0800 YSC-GCD-demo[20862:5290687] 剩餘票數:1 視窗:<NSThread: 0x6040002709c0>{number = 3, name = (null)}
2018-02-23 22:32:29.840234+0800 YSC-GCD-demo[20862:5290689] 剩餘票數:0 視窗:<NSThread: 0x60000046c640>{number = 4, name = (null)}
2018-02-23 22:32:30.044960+0800 YSC-GCD-demo[20862:5290687] 所有火車票均已售完
2018-02-23 22:32:30.045260+0800 YSC-GCD-demo[20862:5290689] 所有火車票均已售完

可以看出,在考慮了執行緒安全的情況下,使用 dispatch_semaphore 機制之後,得到的票數是正確的,沒有出現混亂的情況。我們也就解決了多個執行緒同步的問題。

相關文章