1. 程式人生 > 實用技巧 >GCD訊號量-dispatch_semaphore_t

GCD訊號量-dispatch_semaphore_t

1.GCD訊號量簡介

站在巨人的肩膀上,大家勇於學習即可:https://www.jianshu.com/p/24ffa819379c

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):

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]);
    });
}

}

其執行順序如下圖

執行結果如下圖:

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

一般情況下,傳送訊號和等待訊號是成對出現的。也就是說,一個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];
}


連結:https://www.jianshu.com/p/24ffa819379c