併發 (二):Grand Central Dispatch

向Grand Central Dispatch(大型中樞調度)派發任務

隊列有三種:
1. main thread
2. serial thread
3. concurrent thread

有兩種方法向dispatch queue(調度隊列)提交任務:
- Block對象
- C 函數

之前講過Block對象,現在來說說C函數。

必需提供給GCD的C函數需要是despatch_function_t類型。它在Apple libraries裏邊是這樣被定義的:

typedef void (*dispatch_function_t)(void*)

使用GCD執行UI相關任務

解決方案:
使用dispatch_get_main_queue方法

討論:
UI相關任務是執行在主線程上的,所以main queue是唯一選擇。我們有兩種方式來分發到主隊列:

  1. dispatch_async
    執行一個block對象到調度隊列
  2. dispatch_async_f
    執行一個C函數到調度隊列

以上兩種方法都是異步方法,dispatch_sync方法不能在主線程上使用,因爲它會阻塞進程,導致App進入死鎖。所有通過GCD提交到主隊列的任務都應該是異步提交的。

dispatch_async

它有兩個參數:

  1. 調度隊列句柄(dispatch queue handle)
  2. block對象

來一發無參數無返回值的小例子:

使用Block
 dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_async(mainQueue, ^{
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"GCD" message:@"GCD is amazing!" preferredStyle:UIAlertControllerStyleAlert];

        UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"好的" style:UIAlertActionStyleDefault handler:nil];
        [alertController addAction:okAction];

        [self showViewController:alertController sender:nil];
    });
使用C函數,我們可以提交一個指向application定義的上下文的指針
以下這些內容都寫在AppDelegate.mtypedef struct{
    char *title;
    char *message;
    char *cancelButtonTitle;
}AlertViewData;

void displayAlertView(void *paramContext){
    AlertViewData *alertData = (AlertViewData *)paramContext;
    NSString *title = [NSString stringWithUTF8String:alertData->title];
    NSString *message = [NSString stringWithUTF8String:alertData->message];
    NSString *cancelButtonTitle = [NSString stringWithUTF8String:alertData->cancelButtonTitle];

    [[[UIAlertView alloc]initWithTitle:title message:message delegate:nil cancelButtonTitle:cancelButtonTitle otherButtonTitles:nil, nil]show];
    free(alertData);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    AlertViewData *context = (AlertViewData*)malloc(sizeof(AlertViewData));
    if (context) {
        context->title = "GCD";
        context->message = "GCD is amazing!";
        context->cancelButtonTitle = "OK";

        dispatch_async_f(mainQueue, (void *)context, displayAlertView);
    }
    return YES;
}

//    dispatch_async_f(<#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
參數一:確認隊列;參數二:提交上下文,也就是參數;參數三:調用的C語言方法都是dispatch_function_t類型
我們可以通過NSThread類查看當前隊列和主隊列:
NSLog(@"current thread = %@",[NSThread currentThread]);
NSLog(@"main thread = %@",[NSThread mainThread]);

使用GCD同步執行與UI無關的任務

解決方案:
使用dispatch_sync方法
討論:
任何與UI無關的任務,都可以使用全局併發隊列(globle concurrent queues),它既可以使用同步方法,也允許使用異步方法。
對於同步方法來說,它並不意味着你的應用要等着任務完成後才能繼續,而是說在那個併發線程上的其他任務,需要等待這個隊列上 的當前任務完成後才能繼續執行。你的程序將會一直執行,因爲併發隊列並不執行在主隊列上。(只有一個例外,當一個任務通過dispatch_sync提交到併發隊列concurrent queue或者串行隊列serial queue,如果可能的話,iOS將會讓這個任務執行在當前隊列上,而這個current queue也可能是主線程,它取決於那個時候代碼路徑在哪裏。這個是GCD在編碼時候的一個優化。)
如果你提交一個同步任務到併發隊列,同時提交另一個同步任務到另一個併發隊列,這兩個同步任務對對方來說都是異步執行的,因爲它們是跑在不同的併發隊列上的。
理解以上概念是很重要的。如果你想要確保任務B必須等任務A結束後纔開始,你可以將它們同步的放到同一個隊列上去。
你可以使用dispatch_sync方法執行同步任務在一個調度隊列上,你所需要做的只是,提供一個負責處理任務的隊列,以及一個block代碼來執行在這個隊列上。

下面這個例子,它打印了1-1000兩次,一個隊列完成後接着另一個隊列,並沒有阻塞主線程。我們可以創建一個block對象,來爲我們做計算,並且同步地調用相同的block對象兩次

void (^printFrom1To1000)(void) = ^{
    NSUInteger counter = 0;
    for(counter = 1 ; counter <= 1000 ; counter++){
        NSLog(@"counter = %lu - Thread = %@",(unsigned long)counter,[NSThread currentThread]);
    }
};
//-------------------
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_sync(concurrentQueue, printFrom1To1000);
    dispatch_sync(concurrentQueue, printFrom1To1000);

運行的結果是,兩個隊列依次執行,把第一個執行完後,再打印第二個1-1000,兩個隊列都執行在主線程上,即使你是使用併發隊列來執行這個任務的。原來,這是GCD的優化。dispatch_sync方法將會使用當前線程(就是你用來調度任務的線程),在任何可能的時候,作爲已經被編入GCD的優化的一部分。蘋果官方是這樣說的:
As an optimization, this function invokes the block on the current thread when possible.

C語言的調用方法是這樣子的:

void printFrom1To1000 (void *paramContext){
    NSUInteger counter = 0;
    for(counter = 1 ; counter <= 1000 ; counter++){
        NSLog(@"counter = %lu - Thread = %@",(unsigned long)counter,[NSThread currentThread]);
    }
};
//-------------
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_sync_f(concurrentQueue, NULL, printFrom1To1000);
    dispatch_sync_f(concurrentQueue, NULL, printFrom1To1000);
dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>)

第一個參數是指定隊列的優先級,可選項分別有:DISPATCH_QUEUE_PRIORITY_HIGH、DISPATCH_QUEUE_PRIORITY_DEFAULT、DISPATCH_QUEUE_PRIORITY_LOW
級別越高,任務就會被分配更多的CPU時間片段

第二個參數是保留值,你只需要記住永遠給它賦值0就對了

使用GCD異步執行與UI無關的任務

這是GCD最精華的部分,可以同時在main、serial、concurrent上異步執行block代碼。
解決方案:
1. dispatch_async
2. dispatch_async_f

場景:現在想要從網絡上下載一個圖片,下載完成後顯示給用戶。
思路:

  1. 在併發隊列上異步發動一個block對象;
  2. 一旦進入到這個block中,我們就同步發動另一個block對象,使用dispatch_sync從URL上下載一個圖片。我們這樣做是爲了讓這個併發隊列的其他代碼等待圖片下載完成。因此,我們只是令這個併發隊列等待,而不是其他的隊列。總的來說,我們就是在一個異步的隊列中同步的下載一張圖片,我們需要關注的就是下載的時候不要去阻塞主線程;
  3. 當圖片下載好了之後,我們就會同步的執行一個block對象到主隊列上,是爲了去刷新UI展示圖片。
思路:
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(concurrentQueue, ^{
        __block UIImage *image = nil;
        dispatch_sync(concurrentQueue, ^{
            /*Down load the image here*/
        });
        dispatch_sync(dispatch_get_main_queue(), ^{
            /*Show the image to the user here on the main queque*/
        });
    });
再具體一點

    dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(concurrentQueue, ^{
        __block UIImage *image = nil;
        dispatch_sync(concurrentQueue, ^{
            NSString *urlAsString = @"http://image2.sina.com.cn/ent"\
            "/2004-12-18/1103339247_UQaqtq.jpg";
            NSURL *url = [NSURL URLWithString:urlAsString];
            NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
            NSError *downloadError = nil;

            NSData *imageData = [NSURLConnection sendSynchronousRequest:urlRequest
                                                      returningResponse:nil
                                                                  error:&downloadError];
            if (!downloadError && imageData) {
                imageData = [UIImage imageWithData:imageData];
            }else if(downloadError){
                NSLog(@"Error happened = %@",downloadError);
            }else{
                NSLog(@"No data could get downloaded from the URL");
            }

        });
        dispatch_sync(dispatch_get_main_queue(), ^{
            if (image) {
                /* Show the image */
            }else{
                NSLog(@"Image isn't downloaded. Nothing to display");
            }
        });
    });

使用GCD延後執行任務

解決方案:

  1. dispatch_after
  2. dispatch_after_f

dispatch_after(<#dispatch_time_t when#>, <#dispatch_queue_t queue#>, <#^(void)block#>)
參數:納秒單位的延遲時間,調度的隊列,Block對象;

dispatch_after_f(<#dispatch_time_t when#>, <#dispatch_queue_t queue#>, <#void *context#>, <#dispatch_function_t work#>)
參數:納秒單位的延遲時間,調度的隊列,給C方法使用的上下文,C方法

注意:雖然延遲的單位是納秒,但是它還是由iOS來覺得派遣延遲的粒度大小,因此,延遲的時間也許不會像設定的那麼精確。

代碼示例:

double delayInSeconds = 2.0;
dispatch_time_t delayInNanoSeconds = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_after(delayInNanoSeconds, concurrentQueue, ^{
    /* Perform your operations here */
});

dispatch_time_t類型是一個絕對時間的抽象呈現。爲了得到這個值,你可以使用上面代碼片段中的dispatch_time方法。以下參數是你可以傳遞給dispatch_time方法的:
(Base time,Delta to add to base time)
base time可以設定爲DISPATCH_TIME_NOW。無論最後B或者D設置爲什麼值,最終dispatch_time方法的時間總和就是B+D。
B表示從某一時刻開始的時間,D就是從B開始要延遲的那個時間。
舉例來說:

從現在開始的3s後
dispatch_time_t delayInNanoSeconds = dispatch_time(DISPATCH_TIME_NOW, 3.f * NSEC_PER_SEC);
或者從現在開始的半秒後
dispatch_time_t delayInNanoSeconds = dispatch_time(DISPATCH_TIME_NOW, (1/2.f) * NSEC_PER_SEC);

C語言的實現方式:略,不想寫了

用GCD只執行一次任務

解決方法:
dispatch_once

dispatch_once(<#dispatch_once_t *predicate#>, <#^(void)block#>)
參數:
Token:dispatch_once_t類型的token可以保留第一次從GCD中產生的token;
block 對象;

代碼:

static dispatch_once_t onceToken;
void (^executedOnlyOnce)(void) = ^{
    static NSUInteger numberOfEntries = 0;
    numberOfEntries ++;
    NSLog(@"Excuted %lu time(s)",(unsigned long)numberOfEntries);
};
//--------------------------------

dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_once(&onceToken, ^{
        dispatch_async(concurrentQueue, executedOnlyOnce);
    });

    dispatch_once(&onceToken, ^{
        dispatch_async(concurrentQueue, executedOnlyOnce);
    });

執行的結果是:

Excuted 1 time(s)

實例:使用dispatch_once製作singleton。

-(id)sharedInstance{
    static MySingleton *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [MySingleton new];
    });
    return sharedInstance;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章