GCD信號量-dispatch_semaphore_t

1.GCD信號量簡介

GCD信號量機制主要涉及到以下三個函數:

dispatch_semaphore_create(long value); // 創建信號量
dispatch_semaphore_signal(dispatch_semaphore_t deem); // 發送信號量
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); // 等待信號量

dispatch_semaphore_create(long value);和GCD的group等用法一致,這個函數是創建一個dispatch_semaphore_類型的信號量,并且創建的時候需要指定信號量的大小。
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); 等待信號量。該函數會對信號量進行減1操作。如果減1后信號量小于0(即減1前信號量值為0),那么該函數就會一直等待,也就是不返回(相當于阻塞當前線程),直到該函數等待的信號量的值大于等于1,該函數會對信號量的值進行減1操作,然后返回。
dispatch_semaphore_signal(dispatch_semaphore_t deem); 發送信號量。該函數會對信號量的值進行加1操作。

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

2.用信號量機制使異步線程完成同步操作

眾所周知,并發隊列中的任務,由異步線程執行的順序是不確定的,兩個任務分別由兩個線程執行,很難控制哪個任務先執行完,哪個任務后執行完。但有時候確實有這樣的需求:兩個任務雖然是異步的,但仍需要同步執行。這時候,GCD信號量就可以大顯身手了。

2.1異步函數+并發隊列 實現同步操作

當然,有人說,想讓多個任務同步執行,干嘛非要用異步函數+并發隊列呢?
當然,我們也知道異步函數 + 串行隊列實現任務同步執行更加簡單。不過異步函數 + 串行隊列的弊端也是非常明顯的:因為是異步函數,所以系統會開啟新(子)線程,又因為是串行隊列,所以系統只會開啟一個子線程。這就導致了所有的任務都是在這個子線程中同步的一個一個執行。喪失了并發執行的可能性。雖然可以完成任務,但是卻沒有充分發揮CPU多核(多線程)的優勢。

    // 串行隊列 + 異步 == 只會開啟一個線程,且隊列中所有的任務都是在這個線程執行
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    dispatch_queue_t queue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"111:%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"222:%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"333:%@",[NSThread currentThread]);
    });
}

以上三個任務的執行順序永遠是任務1、任務2、任務3,且永遠是在同一個子線程被執行。如下圖(1、2、3、4、5、6):

圖片.png

2.2 用GCD的信號量來實現異步線程同步操作

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        NSLog(@"任務1:%@",[NSThread currentThread]);
        dispatch_semaphore_signal(sem);
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任務2:%@",[NSThread currentThread]);
        dispatch_semaphore_signal(sem);
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任務3:%@",[NSThread currentThread]);
    });
}

}

其執行順序如下圖:

圖片.png

執行結果如下圖:


圖片.png

通過上面的例子,可以得出結論:

一般情況下,發送信號和等待信號是成對出現的。也就是說,一個dispatch_semaphore_signal(sem);對應一個dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
我們注意到:使用信號量實現異步線程同步操作時,雖然任務是一個接一個被同步(說同步并不準確)執行的,但因為是在并發隊列,并不是所有的任務都是在同一個線程執行的(所以說同步并不準確)。上圖中綠框中的任務2是在線程5中被執行的,而任務1和任務3是在線程4中被執行的。這有別于異步函數+串行隊列的方式(異步函數+ 串行隊列的方式中,所有的任務都是在同一個新線程被串行執行的)。
在此總結下,同步和異步決定了是否開啟新線程(或者說是否具有開啟新線程的能力),串行和并發決定了任務的執行方式——串行執行還是并發執行(或者說開啟多少條新線程)


例如以下情況,分別執行兩個異步的AFN網絡請求,第二個網絡請求需要等待第一個網絡請求響應后再執行,使用信號量的實現:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSString *urlString1 = @"/Users/ws/Downloads/Snip20161223_20.png";
    NSString *urlString2 = @"/Users/ws/Downloads/Snip20161223_21.png";
    // 創建信號量
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        
        [manager POST:urlString1 parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
            
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            NSLog(@"1完成!");
            // 發送信號量
            dispatch_semaphore_signal(sem);
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            NSLog(@"1失敗!");
            // 發送信號量
            dispatch_semaphore_signal(sem);
        }];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 等待信號量
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        
        [manager POST:urlString2 parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
            
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            NSLog(@"2完成!");
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            NSLog(@"2失敗!");
        }];
    });
}

3.用信號量和異步組實現異步線程同步執行

3.1.異步組的常見用法

使用異步組(dispatch Group)可以實現在同一個組內的內務執行全部完畢之后再執行最后的處理。但是同一組內的block任務的執行順序是不可控的。如下:

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        NSLog(@"1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"2");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"3");
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"done");
    });
}

執行結果:


利用異步函數,向全局并發隊列追加處理,block的回調是異步的,多個線程并行執行導致追加的block任務處理順序變化無常,但是執行結果的done肯定是在group內的三個任務執行完畢后在執行。


3.2.信號量+異步組

上面的情況是使用異步函數并發執行三個任務,有時候我們希望使用異步函數并發執行完任務之后再異步回調到當前線程。當前線程的任務執行完畢后再執行最后的處理。這種異步的異步,只使用dispatch group是不夠的,還需要dispatch_semaphore_t(信號量)的加入。

在沒有信號量的情況下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    dispatch_group_t grp = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_group_async(grp, queue, ^{
        NSLog(@"task1 begin : %@",[NSThread currentThread]);
        dispatch_async(queue, ^{
            NSLog(@"task1 finish : %@",[NSThread currentThread]);
        });
    });
    dispatch_group_async(grp, queue, ^{
        NSLog(@"task2 begin : %@",[NSThread currentThread]);
        dispatch_async(queue, ^{
            NSLog(@"task2 finish : %@",[NSThread currentThread]);
        });
    });
    dispatch_group_notify(grp, dispatch_get_main_queue(), ^{
        NSLog(@"refresh UI");
    });
}
執行結果

在有信號量的情況下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    dispatch_group_t grp = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_group_async(grp, queue, ^{
        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        NSLog(@"task1 begin : %@",[NSThread currentThread]);
        dispatch_async(queue, ^{
            NSLog(@"task1 finish : %@",[NSThread currentThread]);
            dispatch_semaphore_signal(sema);
        });
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    });
    dispatch_group_async(grp, queue, ^{
        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        NSLog(@"task2 begin : %@",[NSThread currentThread]);
        dispatch_async(queue, ^{
            NSLog(@"task2 finish : %@",[NSThread currentThread]);
            dispatch_semaphore_signal(sema);
        });
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    });
    dispatch_group_notify(grp, dispatch_get_main_queue(), ^{
        NSLog(@"refresh UI");
    });
}
執行結果

如上圖,只是在43、47、49、53、57、59行加了一些關于信號量的代碼,就實現了我們想要的效果。

雖然我們把兩個任務(假設每個任務都叫做T)加到了異步組中,但是每個任務T又都有一個異步回調T'(這個異步的回調T'操作并不會立即觸發,如果T'是一個網絡請求的異步回調,這個回調的時機取決于網絡數據返回的時間,有可能很快返回,有可能很久返回),相當于每個任務T又都有自己的任務T',加起來就是4個任務。因為異步組只對自己的任務T(block)負責,并不會對自己任務的任務T'(block中的block)負責,異步組把自己的任務執行完后會立即返回,并不會等待自己的任務的任務執行完畢。顯然,上面這種在異步組中再異步的執行順序是不可控的。
不明白的請看下圖:


任務T和任務T'的關系

再不明白請看實例:
例如以下情況:使用線程組異步并發執行兩個AFN網絡請求,然后網絡請求不管成功或失敗都會各自回調主線程去執行success或者failure的block中的任務。等到都執行完網絡請求的block中的異步任務后,再發出notify,通知第三個任務,也就是說,第三個任務依賴于前兩個網絡請求的異步回調執行完畢(注意不是網絡請求,而是網絡請求的異步回調,注意區分網絡請求和網絡請求的異步回調,網絡請求是一個任務,網絡請求的異步回調又是另一個任務,因為是異步,所以網絡請求很快就結束了,而網絡請求的異步回調是要等待網絡響應的)。但兩個網絡請求的異步回調的執行順序是隨機的,即,有可能是第二個網絡請求先執行block回調,也有可能是第一個網絡請求先執行block回調。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSString *appIdKey = @"8781e4ef1c73ff20a180d3d7a42a8c04";
    NSString* urlString_1 = @"http://api.openweathermap.org/data/2.5/weather";
    NSString* urlString_2 = @"http://api.openweathermap.org/data/2.5/forecast/daily";
    NSDictionary* dictionary =@{@"lat":@"40.04991291",
                                @"lon":@"116.25626162",
                                @"APPID" : appIdKey};
    // 創建組
    dispatch_group_t group = dispatch_group_create();
    // 將第一個網絡請求任務添加到組中
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 創建信號量
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        // 開始網絡請求任務
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        [manager GET:urlString_1
          parameters:dictionary
            progress:nil
             success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                 NSLog(@"1任務成功");
                 // 如果請求成功,發送信號量
                 dispatch_semaphore_signal(semaphore);
             } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                 NSLog(@"1任務失敗");
                 // 如果請求失敗,也發送信號量
                 dispatch_semaphore_signal(semaphore);
             }];
        // 在網絡請求任務成功/失敗之前,一直等待信號量(相當于阻塞,不會執行下面的操作)
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    });
    // 將第二個網絡請求任務添加到組中
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 創建信號量
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        // 開始網絡請求任務
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        [manager GET:urlString_2
          parameters:dictionary
            progress:nil
             success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                 NSLog(@"2任務成功");
                 // 如果請求成功,發送信號量
                 dispatch_semaphore_signal(semaphore);
             } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                 NSLog(@"2任務失敗");
                 // 如果請求失敗,也發送信號量
                 dispatch_semaphore_signal(semaphore);
             }];
        // 在網絡請求任務成功/失敗之前,一直等待信號量(相當于阻塞,不會執行下面的操作)
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    });
    dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"1和2已經完成,執行任務3。");
    });
}

上面代碼兩個線程各自創建了一個信號量,所以任務1和任務2的執行順序具有隨機性,而任務3的執行肯定會是在任務1和任務2執行完畢之后再執行。如下圖:

執行結果

其實,這種操作也可以用dispatch_group_enter(dispatch_group_t group) 和 dispatch_group_leave(dispatch_group_t group)來實現:

   dispatch_group_t group =dispatch_group_create();
   dispatch_queue_t globalQueue=dispatch_get_global_queue(0, 0);
   
   dispatch_group_enter(group);
   
   //模擬多線程耗時操作
   dispatch_group_async(group, globalQueue, ^{
       dispatch_async(globalQueue, ^{
           dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
               NSLog(@"%@---block1結束。。。",[NSThread currentThread]);
               dispatch_group_leave(group);
           });
       });
       NSLog(@"%@---1結束。。。",[NSThread currentThread]);
   });
   
   dispatch_group_enter(group);
   //模擬多線程耗時操作
   dispatch_group_async(group, globalQueue, ^{
       dispatch_async(globalQueue, ^{
           dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
               NSLog(@"%@---block2結束。。。",[NSThread currentThread]);
               dispatch_group_leave(group);
           });
       });
       NSLog(@"%@---2結束。。。",[NSThread currentThread]);
   });
   
   dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
       NSLog(@"%@---全部結束。。。",[NSThread currentThread]);
   });
執行結果

如上圖,使用dispatch_group_enter()和dispatch_group_leave()函數后,我們也能保證dispatch_group_notify()中的任務總是在最后被執行。
另外,我們必須保證dispatch_group_enter()和dispatch_group_leave()是成對出現的,不然dispatch_group_notify()將永遠不會被調用。


4.利用dispatch_semaphore_t將數據追加到數組

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:100];
    // 創建為1的信號量
    dispatch_semaphore_t sem = dispatch_semaphore_create(1);
    for (int i = 0; i < 10000; i++) {
        dispatch_async(queue, ^{
            // 等待信號量
            dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
            [arrayM addObject:[NSNumber numberWithInt:i]];
            NSLog(@"%@",[NSNumber numberWithInt:i]);
            // 發送信號量
            dispatch_semaphore_signal(sem);
        });
    }

使用并發隊列來更新數組,如果不使用信號量來進行控制,很有可能因為內存錯誤而導致程序異常崩潰。如下:


5.工作中信號量應用

一般情況下,-(NSString *)getSSKToken會立即返回。但加入了信號量,就會阻塞住當前線程,直到網絡返回后,這個方法才返回。這是因為當時業務需要而產生的一種特殊的應用場景。正常情況下,不建議大家這樣操作,否則很容易阻塞住主線程。關于信號量的應用常見于上面提到的和異步組的搭配使用。

- (NSString *)getSSOToken {
    NSURLSession *session = [NSURLSession sharedSession];
    NSString *accessToken = [AlilangSDK sharedSDKInstance].accessToken;
    if (!accessToken) {
        return nil;
    }
    NSString *urlString = [NSString stringWithFormat:@"%@?appcode=%@&accesstoken=%@",SSO_TOKEN_URL,ALY_BUCAPPCODE,accessToken];
    NSURL *url = [NSURL URLWithString:urlString];
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    NSURLSessionTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.SSOTokenDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
            ALYLog(@"dict == %@",self.SSOTokenDictionary);
        });
        dispatch_semaphore_signal(sem);
    }];
    [task resume];
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    return [self.SSOTokenDictionary objectForKey:SSO_TOKEN];
}

如果有技術問題,歡迎加入QQ群進行交流,群聊號碼:194236752。

文/VV木公子(簡書作者)
PS:如非特別說明,所有文章均為原創作品,著作權歸作者所有,轉載請聯系作者獲得授權,并注明出處,所有打賞均歸本人所有!

如果您是iOS開發者,或者對本篇文章感興趣,請關注本人,后續會更新更多相關文章!敬請期待!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1 GCD 術語 1.1 Serial vs. Concurrent 串行 vs. 并發 概念:該術語描述執行當前...
    NinthDay閱讀 3,962評論 2 38
  • Managing Units of Work(管理工作單位) 調度塊允許您直接配置隊列中各個工作單元的屬性。它們還...
    edison0428閱讀 8,032評論 0 1
  • 昨天發生了一件事情,以前也發生過但是沒有當回事。 事情是這樣的,有一個報表,內容上有一個字段的數據在系統里是沒有他...
    BigQ個人成長閱讀 570評論 0 50
  • 是之一但不是唯一。 央視這些年出了一些寓教于樂的綜藝,比如《中國成語大會》 《中國詩詞大會》 一定程度上吸引了現代...
    喲主公電閃閱讀 318評論 0 0
  • 1 圣誕節的早上,顧南站在陶瓷店旁邊,等到嚴師傅走了過來。嚴師傅睡眼惺忪,伸手打開店門,顧南突然發出聲音,把他嚇得...
    沉墨先生閱讀 1,904評論 0 4