iOS中的多執行緒(關於GCD訊號量)

語言: CN / TW / HK

highlight: a11y-dark theme: cyanosis


「這是我參與11月更文挑戰的第10天,活動詳情檢視:2021最後一次更文挑戰

GCD 訊號量

什麼是 Dispatch Semaphore

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


Dispatch Semaphore 的方法

  • 建立訊號量 建立一個 dispatch_semaphore_ 型別的訊號量,並且建立的時候需要指定訊號量的大小,當傳遞的值小於0,訊號量將初始化失敗返回NULL js dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

  • 傳送訊號量 傳送一個訊號,會對訊號量進行加 1

js dispatch_semaphore_signal(semaphore);

  • 等待訊號量

該函式會對訊號量進行減 1。如果減 1 後訊號量小於 0(即減1前訊號量值為0),那麼該函式就會一直等待,也就是不返回(相當於阻塞當前執行緒),直到該函式等待的訊號量的值大於等於 1,該函式會對訊號量的值進行減1操作,然後返回。

js dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

通常等待訊號量和傳送訊號量的函式是成對出現的。併發執行任務時候,在當前任務執行之前,用 dispatch_semaphore_wait 函式進行等待(阻塞),直到上一個任務執行完畢後且通過 dispatch_semaphore_signal 函式傳送訊號量(使訊號量的值加1),dispatch_semaphore_wait 函式收到訊號量之後判斷訊號量的值大於等於1,會再對訊號量的值減1,然後當前任務可以執行,執行完畢當前任務後,再通過 dispatch_semaphore_signal 函式傳送訊號量(使訊號量的值加1),通知執行下一個任務......如此一來,通過訊號量,就達到了併發佇列中的任務同步執行的要求


用訊號量機制使非同步執行緒完成同步操作

在併發佇列中的任務,由非同步執行緒執行的順序是不確定的,兩個任務分別由兩個執行緒執行,很難控制哪個任務先執行完,哪個任務後執行完。但有時候確實有這樣的需求:兩個任務雖然是非同步的,但仍需要同步執行。這時候,GCD訊號量就可以大顯身手了。

  • 非同步函式+併發佇列 實現同步操作

    我們知道非同步函式 + 序列佇列實現任務同步執行更加簡單。不過非同步函式 + 序列佇列的弊端也是非常明顯的:因為是非同步函式,所以系統會開啟新(子)執行緒,又因為是序列佇列,所以系統只會開啟一個子執行緒。這就導致了所有的任務都是在這個子執行緒中同步的一個一個執行。喪失了併發執行的可能性。雖然可以完成任務,但是卻沒有充分發揮CPU多核(多執行緒)的優勢

    示例: ```js - (void)touchesBegan:(NSSet )touches withEvent:(UIEvent )event{ dispatch_queue_t queue = dispatch_queue_create("com.gcd.serialQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_async(queue, ^{
        NSLog(@"任務1:%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務2:%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務3:%@",[NSThread currentThread]);
    });
    

    }
    ``` log:(三個任務的執行順序永遠是任務1、任務2、任務3,且永遠是在同一個子執行緒被執行)

    Snip20211108_3.png

  • 用GCD的訊號量來實現非同步執行緒同步操作

    使用訊號量實現非同步執行緒同步操作時,雖然任務是一個接一個被同步(說同步並不準確)執行的,但因為是在併發佇列,並不是所有的任務都是在同一個執行緒執行的(所以說同步並不準確)。log中綠框中的任務3是線上程8中被執行的,而任務1和任務2是線上程3中被執行的。這有別於非同步函式+序列佇列的方式(非同步函式+ 序列佇列的方式中,所有的任務都是在同一個新執行緒被序列執行的)

    ```js - (void)touchesBegan:(NSSet )touches withEvent:(UIEvent )event{

    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    
    NSLog(@"1、任務1加入併發佇列");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
        NSLog(@"任務1:%@",[NSThread currentThread]);
    
        NSLog(@"3、任務1進行訊號量加1");
        dispatch_semaphore_signal(sem);
    });
    
    NSLog(@"2、任務1進行訊號量減1");
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    
    NSLog(@"4、任務2加入併發佇列");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
        NSLog(@"任務2:%@",[NSThread currentThread]);
    
        NSLog(@"6、任務2進行訊號量加1");
        dispatch_semaphore_signal(sem);
    });
    
    NSLog(@"5、任務2進行訊號量減1");
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    
    NSLog(@"7、任務3加入併發佇列");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任務3:%@",[NSThread currentThread]);
    
    });
    

    } ``` log:(點選事件3次的log,去除了執行順的log) Snip20211108_5.png

    log:(單次點選事件的log,包含執行順序) Snip20211108_9.png


Dispatch Semaphore 的使用

訊號量的使用前提是:想清楚需要處理哪個執行緒阻塞,需要哪個執行緒繼續執行,然後使用訊號量

  • 保持執行緒同步,將非同步執行任務轉換為同步執行任務

    示例: ```js NSLog(@"currentThread---%@",[NSThread currentThread]);

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    //建立一個 Semaphore, 並初始化訊號的總量 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    __block int number = 0;

    //非同步執行 dispatch_async(queue, ^{ for (NSInteger i = 0; i<3; i++) { NSLog(@"非同步執行----%@",[NSThread currentThread]); } number = 100;

    //傳送一個訊號,讓訊號總量加 1
    dispatch_semaphore_signal(semaphore);
    

    });

    NSLog(@"非同步執行已新增併發佇列");

    //總訊號量減 1 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    NSLog(@"執行完畢,number = %d",number); ``` log:

    Snip20211108_1.png

    通過log梳理執行順序:
    1、semaphore 初始化建立訊號量為 0
    2、非同步執行加入佇列後,不做等待,接著執行 dispatch_semaphore_wait,這時訊號量由0變為-1,當前執行緒進入等待狀態
    3、非同步執行任務,通過 dispatch_semaphore_signal 方法訊號量由-1變為0,當前被阻塞的執行緒恢復繼續執行
    4、最後輸出:執行完畢,number = 100,這樣就實現了執行緒同步,將非同步執行任務轉換為同步執行任務

  • 保證執行緒安全,為執行緒加鎖

    示例: ```js - (void)viewDidLoad{ semaphore = dispatch_semaphore_create(1);

    self.ticketSurplusCount = 5;
    
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_CONCURRENT);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue, ^{
        [weakSelf saleTicketSafe];
    });    
    dispatch_async(queue, ^{
        [weakSelf saleTicketSafe];
    });
    

    }

    • (void)saleTicket{ while (1) { // 相當於加鎖 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); if (self.ticketSurplusCount > 0) { self.ticketSurplusCount--; NSLog(@"%@", [NSString stringWithFormat:@"剩餘票數:%ld 售票員:%@", (long)self.ticketSurplusCount, [NSThread currentThread]]); [NSThread sleepForTimeInterval:0.2]; } else {            NSLog(@"票賣完了"); // 相當於解鎖 dispatch_semaphore_signal(semaphore); break; } // 相當於解鎖 dispatch_semaphore_signal(semaphore); } } ``` log:

    Snip20211108_10.png