12/08/2018, 10:48

Grand Central Dispatch - Part 3: Dispatch group

Tiếp tục từ phần trước: https://viblo.asia/thevinh92/posts/QWkwGna3M75g Dựa theo tài liệu: http://www.raywenderlich.com/63338/grand-central-dispatch-in-depth-part-2 Ở các phần trước, chúng ta đã nghiên cứu về concurrency, threading và cách làm việc của GCD. Chúng ta đã làm singleton PhotoManager ...

Tiếp tục từ phần trước: https://viblo.asia/thevinh92/posts/QWkwGna3M75g Dựa theo tài liệu: http://www.raywenderlich.com/63338/grand-central-dispatch-in-depth-part-2

Ở các phần trước, chúng ta đã nghiên cứu về concurrency, threading và cách làm việc của GCD. Chúng ta đã làm singleton PhotoManager trở thành thread safe bằng việc instantiate nó sử dụng dispatch_one và làm cho việc đọc và ghi của Photos là thread safe bằng cách kết hợp giữa dispatch_barrier_async và dispatch_sync. Ngoài những điều đó, ta cũng đã cải thiện app về mặt UX thông qua việc sử dụng một prompt với dispatch_after, và giảm tải công việc instactiate 1 view controller để thực hiện 1 nhiệm vụ chuyên sâu cho CPU với dispatch_async.

Bây giờ ta sẽ nghiên cứu sâu hơn về GCD:

Sửa việc Popup bật lên quá sớm

Bạn có thâer đã nhận thấy rằng khi ta add photo với option Le Internet, 1 cái popup kiểu UIAlertView được bật lên trước khi các ảnh được download xong, như hình dưới đây:

Screen-Shot-2014-01-17-at-5.49.51-PM-308x500.png

Các lỗi nằm ở hàm downloadPhotoWithCompletionBlock: của PhotoManagers:

- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
    __block NSError *error;

    for (NSInteger i = 0; i < 3; i++) {
        NSURL *url;
        switch (i) {
            case 0:
                url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
                break;
            case 1:
                url = [NSURL URLWithString:kSuccessKidURLString];
                break;
            case 2:
                url = [NSURL URLWithString:kLotsOfFacesURLString];
                break;
            default:
                break;
        }

        Photo *photo = [[Photo alloc] initwithURL:url
                              withCompletionBlock:^(UIImage *image, NSError *_error) {
                                  if (_error) {
                                      error = _error;
                                  }
                              }];

        [[PhotoManager sharedManager] addPhoto:photo];
    }

    if (completionBlock) {
        completionBlock(error);
    }
}

Ở đây ta gọi completionBlock ở cuối method - ta đang giả định tất cả các ảnh tải về đã hoàn thành. Nhưng thật không may rằng không có gì đảm bảo rằng tất cả các ảnh tải về đã hoàn thành ở thời điểm này. Method khởi tạo của lớp Photo bắt đầu download 1 file từ URL về và return ngày lập tức trước khi download kết thúc. Nói cách khác, downloadPhotoWithCompletionBlock: gọi completion block của nó ở cuối cùng, như thể tất cả các straight-line code đồng bộ ở phần giữa và các methode được gọi trong đó đã hoàn thành công việc của mình. Tuy nhiên, -[Photo initWithURL:withCompletionBlock:] là kiểu không đồng bộ và return nagyf lập tức -> phương pháp này sẽ không chạy được.

Thay vào đó, downloadPhotoWithCompletionBlock: nên gọi completion block chỉ sau khi tất cả các task download ảnh đã gọi completion block của bản thân nó. Câu hỏi đặt ra là: làm thế nào để ta theo dõi đồng thời được các sự kiện không đồng bộ? Bạn không biết nó kết thúc lúc nào hay thứ tự kết thúc của nó. Có lẽ ta ênn viết 1 số hacky code mà sử dụng 1 số biến bool để theo dõi từng download, "but that doesn’t scale well, and frankly, it makes for pretty ugly code." May mắn ở chỗ, việc theo dõi hoàn thành không đồng bộ nhiều task 1 lúc ntn lại chính là những gì mà dispatch groups được thiết kế.

Dispatch Groups

Dispatch Groups sẽ thông báo khi toàn bộ 1 nhóm các task được hoàn thành. Những task này có thể là đồng bộ hoặc không đồng bộ và thậm chí có thể được theo dõi từ các hàng đợi khác nhau. Dispatch group cũng thông báo cho bạn theo cách thức đồng bộ hoặc không đồng bộ khi tất cả các sự kiện của nhóm được hoàn thành. Nếu khi mà các mục được theo dõi trên các hàng đợi khác nhau, 1 instance của dispatch_group_t sẽ theo dõi những task đó.

GCD API cung cấp 2 cách để được thông báo khi tất các các sự kiện trong nhóm hoàn thành: thứ nhất đó là dispatch_group_wait, là 1 function mà block thread hiện tại và chờ cho đến khi tất cả các task trong group đã hoàn thành, hoặc đến khi timeout xảy ra. Đây chính xác là những gì chúng ta mong muốn trong trường hợp này. Mở file PhotoManager.m và thay thế downloadPhotosWithCompletionBlock: như sau:

- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1

        __block NSError *error;
        dispatch_group_t downloadGroup = dispatch_group_create(); // 2

        for (NSInteger i = 0; i < 3; i++) {
            NSURL *url;
            switch (i) {
                case 0:
                    url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
                    break;
                case 1:
                    url = [NSURL URLWithString:kSuccessKidURLString];
                    break;
                case 2:
                    url = [NSURL URLWithString:kLotsOfFacesURLString];
                    break;
                default:
                    break;
            }

            dispatch_group_enter(downloadGroup); // 3
            Photo *photo = [[Photo alloc] initwithURL:url
                                  withCompletionBlock:^(UIImage *image, NSError *_error) {
                                      if (_error) {
                                          error = _error;
                                      }
                                      dispatch_group_leave(downloadGroup); // 4
                                  }];

            [[PhotoManager sharedManager] addPhoto:photo];
        }
        dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5
        dispatch_async(dispatch_get_main_queue(), ^{ // 6
            if (completionBlock) { // 7
                completionBlock(error);
            }
        });
    });
}
  1. Khi bạn sử dụng dispatch_group_wait (1 cách đồng bộ) mà sẽ block thread hiện tại, bạn phải sử dụng dispatch_async để đặt toàn bộ method vào một background queue để chắc chắn rằng bạn sẽ không block nhầm phải main thread.

  2. Tạo ra 1 dispatch group mà hành xử có phần giống như bộ đếm số lượng các task chưa kết thúc

  3. dispatch_group_enter thông báo 1 cách thủ công cho 1 group rằng 1 task nào đó đã bắt đầu. Bạn phải cân bằng số lần gọi dispatch_group_enter với số lần gọi dispatch_group_leave, nếu không bạn sẽ nhận được những lỗi crash kỳ lạ.

  4. Ở đây, bạn thông báo 1 cách thủ công cho 1 group rằng công việc đã hoàn thành. Một lần nữa, bạn phải cẩn bằng tất cả các group enters bằng với số lượng của các group leaves.

  5. dispatch_group_wait: chờ cho đến khi tất cả các task đã được hoàn thành hoặc đến khi hết thời gian. Nếu hết thời gian trước khi tất cả các sự kiện hoàn thành, function sẽ trả về 1 kết quả khác không (non-zero). Bạn có thể đặt nó vào 1 conditional block để check xem thời gian chờ đã hết hạn; tuy nhiên, trong trường hợp này bạn chỉ định cho nó để chờ mãi mãi bằng cách DISPATCH_TIME_FOREVER. Có nghĩa là, Nó sẽ đợi mãi mãi, điều đó là tốt, bởi vì việc hoàn thành của việc tạo ra các bức ảnh sẽ luôn luôn hoàn tất.

  6. Ở thời điểm này, bạn được đảm bảo rằng tất các image task đã hoàn thành hoặc hết thời gian. Sau đó bạn thực hiện việc call back tới main queue để chạy completion block. Điều này sẽ nối thêm công việc vào main thread để được thực hiện vào 1 thời điểm sau đó.

  7. Cuối cùng, kiểm tra completion block có phải là nil hay không, nếu không, chạy nó.

Chú ý: Nếu như mạng quá nhanh để phân biệt được khi nào completion block chạy và bạn chạy app trên device, bạn có thể chắc chắn rằng nó sẽ chyaj bằng cách thiết lập 1 số network setting trong Developer Section của Setttings app. Chỉ cần vào phần Network Link Conditioner, bật lên và chọn 1 cấu hình "Very Bad Network".

Khi nào và sử dụng dispatch groups như thế nào trong 1 số trường hợp queue:

  • Custom Serial Queue: Tương đối tốt
  • Main Queue (Serial): tương đối tốt nhưng cần thận trọng khi chờ đợi đồng bộ để hoàn thành tất cả các công việc. Tuy nhiên, mô hình bất đồng bộ lại là 1 cách hấp dẫn để cập nhật UI khi 1 số task tốn thời gian như network call chạy xong.
  • Concurrent Queue: Đây cũng là 1 ứng viên tốt cho dispatch group và completion notification.

Dispatch group, cách 2

Cách làm như trên đã là tương đối tốt, tuy nhiên vẫn có 1 chút vụng về khi phải dispatch không đồng bộ vào 1 queue khác và block nó sử dụng dispatch_group_wait. Chúng ta có cách khác... TÌm method downloadPhotosWithCompletionBlock: trong PhotoManager.m và thay thế nó bằng cách sau:

- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
    // 1
    __block NSError *error;
    dispatch_group_t downloadGroup = dispatch_group_create();

    for (NSInteger i = 0; i < 3; i++) {
        NSURL *url;
        switch (i) {
            case 0:
                url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
                break;
            case 1:
                url = [NSURL URLWithString:kSuccessKidURLString];
                break;
            case 2:
                url = [NSURL URLWithString:kLotsOfFacesURLString];
                break;
            default:
                break;
        }

        dispatch_group_enter(downloadGroup); // 2
        Photo *photo = [[Photo alloc] initwithURL:url
                              withCompletionBlock:^(UIImage *image, NSError *_error) {
                                  if (_error) {
                                      error = _error;
                                  }
                                  dispatch_group_leave(downloadGroup); // 3
                              }];

        [[PhotoManager sharedManager] addPhoto:photo];
    }

    dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ // 4
        if (completionBlock) {
            completionBlock(error);
        }
    });
}
  1. Trong cách này, bạn không cần phải đặt method bên trong async call bởi vì chúng ta không hề block the main thread.
  2. Tương tự như enter method ở trên
  3. tương tự như leave method ở trên
  4. dispatch_group_notify đóng vai trò như một completion block bất đồng bộ. Đoạn code này thực thi khi không còn gì ở bên trong dispatch group nữa và đến lượt completion block chạy. Bạn cũng có thể chỉ định trên hàng đợi nào chạy completion code của bạn. ở đây là main queue.

Cách tiếp cận này rõ ràng hơn để xử lý công việc này, và nó không hề block bất kỳ thread nào.

0