12/08/2018, 11:42

Grand Central Dispatch - Part 4

Tiếp theo từ phần trước: https://viblo.asia/thevinh92/posts/NznmMdJ6vr69 Dựa theo hướng dẫn và tài liệu: http://www.raywenderlich.com/63338/grand-central-dispatch-in-depth-part-2 The Perils of Too Much Concurrency Cùng xem lại method downloadPhotosWithCompletionBlock in PhotoManager. Bạn có ...

Tiếp theo từ phần trước: https://viblo.asia/thevinh92/posts/NznmMdJ6vr69

Dựa theo hướng dẫn và tài liệu: http://www.raywenderlich.com/63338/grand-central-dispatch-in-depth-part-2

The Perils of Too Much Concurrency

Cùng xem lại method downloadPhotosWithCompletionBlock in PhotoManager. Bạn có thể thấy rằng có 1 vòng for lặp qua 5 lần và download riêng rẽ 5 ảnh về. Công việc của ta ở đây là làm sao để chạy vòng for này 1 cách song song và cố gắng tăng tốc app.

Đó chính là dispatch_apply. dispatch_apply hoạt động như 1 vòng for mà chạy các vòng lặp khác nhau 1 cách đồng thời. Function này là đồng bộ (synchronous) nên nó hoạt động như 1 vòng for bình thường, dispatch_apply return chỉ khi tất cả công việc đã hoàn tất.

Cần phải cẩn thận trong việc tìm ra số lần lặp tối đa cho bất kỳ số lượng công việc cho trước trong block, vì quá nhiều lần lặp lại cho 1 lượng nhỏ công việc có thể tạo ra rất nhiều chi phí và nó khiến cho việc gọi đồng thời trở nên vô nghĩa. Kỹ thuật striding sẽ giúp bạn thoát khỏi nó. Đây là nơi cho mỗi lần lặp bạn làm nhiều phần của công việc.

Khi nào thì thích hợp để sử dùng dispatch_apply?

  • Custom Serial Queue: hoàn toàn phủ nhận việc sử dụng dispatch_apply, chỉ cần sử dụng 1 vòng lặp for bình thường.
  • Main Queue(Serial): Tương tự như trên.
  • Concurrent Queue: Đây là 1 lựa chọn tốt cho việc looping đồng thời, đặc biệt là nếu bạn cần phỉa theo dõi tiến độ của công việc.

Quay trở lại method downloadPhotosWithCompletionBlock và thay nó với cách Implement sau:

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

    dispatch_apply(3, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t 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);
        Photo *photo = [[Photo alloc] initwithURL:url
                              withCompletionBlock:^(UIImage *image, NSError *_error) {
                                  if (_error) {
                                      error = _error;
                                  }
                                  dispatch_group_leave(downloadGroup);
                              }];

        [[PhotoManager sharedManager] addPhoto:photo];
    });

    dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
        if (completionBlock) {
            completionBlock(error);
        }
    });
}

Vòng lặp của bạn giờ đây đã chạy đồng thời. Ở code trên, trong việc gọi dispatch_apply: bạn cần cung cấp số lần lặp trong tham số đầu tiên, queue để thực hiện task trong tham số thứ 2 và khối hành động trong tham số thứ 3.

Nên lưu ý rằng mặc dù code của bạn add photo một cách thread safe, thứ tự của các ảnh đc add vào có thể khác nhau tuỳ thuộc vào thread nào kết thúc trước.

Chạy thử code mới này trên thiết bị và bạn sẽ thấy thỉnh thoảng, chương trình chạy nhanh hơn 1 chút, nhưng những công việc này có đáng không?

Trên thực tế, nó không tốt trong trường hợp này:

  • Thời gian để viết 1 app là giới hạn - đừng lãng phí thời gian vào việc tối ưu hoá code trong khi nó có thể dẫn đến việc crash app. Nếu bạn đang tìm cách tối ưu hoá, hãy tối ưu hoá cái gì đáng chú ý hơn và xứng đáng với thời gian của bạn. Tìm methods mà thời gian chạy dài nhất bằng cách profiling app với Instruments.
  • Thông thường, tối ưu hoá code làm cho code của bạn phức tạp hơn cho những developers khác và cả chính bạn. Hãy chắc chắn rằng sự phức tạp thêm vào là xứng đáng với giá trị mà nó mang lại.

Blocking - The right way

Bạn có biết rằng Xcode có chức năng testing. Viét và chạy test là rất quan trọng khi xây dựng các quan hệ phức tạp trong code. Testing trong Xcode đc thực hiện trên các lớp con của XCTestCase và chạy bất kỳ method nào mà signature của nó bắt đầu với test. Testing được tính toán trên main thread, vì vậy bạn giả định rằng tát cả các test đều xảy ra 1 cách tuần tự. Ngay sau khi 1 test method được hoàn thiện, XCTest mothods sẽ xem xét test đó đã được kết thúc và chuyển sang test tiếp theo. Điều đó có nghĩa rằng bất kỳ code không đồng bộ nào từ các test trước đó sẽ chạy tiếp trong khi test tiếp theo vẫn đang chạy.

Networking code thì thường là không đồng bộ, khi bạn không muốn block main thread trong khi đang thực hiện 1 network fetch. Vì thế, cùng với thực tế là test sẽ kết thúc khi test method kết thúc, có thể khiến nó khó khăn trong việc test networking code. Đó là, trừ khi bạn block main thread ben trong test method cho đến khi networking code kết thúc.

Xem method downloadingImageURLWithString: trong file GooglyPuffTests.m sẽ thấy như sau:

- (void)downloadImageURLWithString:(NSString *)URLString
{
    NSURL *url = [NSURL URLWithString:URLString];
    __block BOOL isFinishedDownloading = NO;
    __unused Photo *photo = [[Photo alloc]
                             initwithURL:url
                             withCompletionBlock:^(UIImage *image, NSError *error) {
                                 if (error) {
                                     XCTFail(@"%@ failed. %@", URLString, error);
                                 }
                                 isFinishedDownloading = YES;
                             }];

    while (!isFinishedDownloading) {}
}

Đây là 1 cách tiếp cận ngây thơ để test các networking code không đồng bộ. Vòng lặp while ở cuối của function chờ cho đến khi biến Boolean isFinishedDownloading trở thành true, xảy ra trong completion block. Hãy thử xem những tác động của nó:

Chạy test bằng cách click vào Product/Test hoặc sử dụng ⌘+U. Sau khi chạy test, bạn có thể thấy được việc sử dụng CPU trong Xcode trên navigator debug. Sự thiết kế kém này chính là 1 spinlock cơ bản. Nó ko có giá trị thực tế bởi vì bạn đang lãng phí chu kỳ CPU trong vòng lặp while. Bạn có thể phải cần đến network link conditioner như đã giải thích ở trên để thấy rõ vấn đề này hơn, bởi nếu mạng của bạn quá nhanh thì điều đó chỉ xảy ra trong 1 khoảng thời gian rất nhỏ.

Bạn cần 1 giải pháp có khả năng mở rộng để block 1 thread cho đến khi tài nguyên sẵn sàng:

Semaphores

Semaphores là 1 khái niệm về threading cổ điển được giới thiệu bởi Edsger W.Dijkstra. Semaphores là 1 chủ đề phức tạp bởi vì nó được xây dựng dựa trên các chức năng phức tạp của hệ điều hành.

Semaphores cho phép bạn kiểm soát sự truy cập của nhiều người vào 1 số lượng hữu hạn của tài nguyên. vids dụ, nếu bạn tạo 1 semaphore với 1 pool gồm 2 tài nguyên, nhiều nhất chỉ 2 thread có thể truy cập vào critical section cùng 1 lúc. Các item khác muốn sử dụng tài nguyên phải chờ đợi trong một FIFO queue.

Open GooglyPuffTests.m và thay thế downloadImageURLWithString: bằng cách implementation như sau:

- (void)downloadImageURLWithString:(NSString *)URLString
{
    // 1
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    NSURL *url = [NSURL URLWithString:URLString];
    __unused Photo *photo = [[Photo alloc]
                             initwithURL:url
                             withCompletionBlock:^(UIImage *image, NSError *error) {
                                 if (error) {
                                     XCTFail(@"%@ failed. %@", URLString, error);
                                 }

                                 // 2
                                 dispatch_semaphore_signal(semaphore);
                             }];

    // 3
    dispatch_time_t timeoutTime = dispatch_time(DISPATCH_TIME_NOW, kDefaultTimeoutLengthInNanoSeconds);
    if (dispatch_semaphore_wait(semaphore, timeoutTime)) {
        XCTFail(@"%@ timed out", URLString);
    }
}

Semaphores làm việc như sau:

  • 1: Tạo semaphore. Parameter chỉ ra giá trị khởi đầu của semaphore. Số này là số lượng những thứ có thể truy cập vào semaphore mà ko cần phải tăng nó từ đầu. (Chú ý rằng tăng dần 1 semaphore được gọi là báo hiệu nó).
  • 2: Trong completion block, bạn thông báo cho các semaphore mà bạn không còn cần đến resource. Điều này gia tăng số đếm semaphore và báo hiệu rằng semaphore đang có sẵn để các resource khác mà muốn nó.
  • 3: Đoạn này đợi trên semaphore, với 1 tham số timeout. Nó block thread hiện tạo cho đến khi semaphore được gửi tín hiệu. code trả về 1 số khác không của function này nghĩa là đã đạt đến timeout. Trong trường hợp này, test đã bị failed vì có vẻ như network không nên mất đến 10s để trả về.

Run test lại 1 lần nữa. Miễn là bạn có 1 kết nối mạng để làm việc, test sẽ thành công trong 1 cách kịp thời. Đặc biệt chú ý đến việc sử dụng CPU trong trường hợp này so với việc sử dụng spinlock trước đó.

Rút dây mạng ra và chạy test lại lần nữa, nếu bạn chạy trên device, để chế độ airplan. Khi chạy test sẽ bị fail sau 10s. These are rather trivial tests, but if you are working with a server team then these basic tests can prevent a wholesome round of finger-pointing of who is to blame for the latest network issue.

Working With Dispatch Sources

Một tính năng thú vị của GCD là Dispatch Sources, mà về cơ bản là một "grab-bag" của các chứ năng cơ bản cấp thấp giúp bạn trả lời hoặc theo dõi các Unix signals, file descriptors, Mach ports, VFS Nodes, và các thứ được che khuất khác. Tất cả điều này vượt ra khỏi phạm vi của tutorial, tuy nhiên bạn sẽ hiểu đc 1 chút bằng cách implement 1 đối tuượng dispatch source và sử dụng nó 1 cách khá đặc biệt.

Người dùng lần đầu sẽ khá mất thời gian về cách sử dụng source, do đó, điều đầu tiên bạn cần phải biết là dispatch_source_create hoạt động như thế nào. Function prototype cho việc create 1 source:

dispatch_source_t dispatch_source_create(
   dispatch_source_type_t type,
   uintptr_t handle,
   unsigned long mask,
   dispatch_queue_t queue);

Parameter đầu tiên dispatch_source_type_t: đây là thông số quan trọng nhất vì nó đưa ra parameter là handle và mask gì. Bạn cần phải tham khảo Xcode documentation để thấy những tuỳ chọn có sẵn cho mỗi tham số.

1 dispatch source mà giám sát quá trình hiện tại cho signals. handle là 1 signal number (int). mask là 1 unused (cho phép bằng 0).

1 danh sách các Unix signal có thể thấy ở file header signal.h . Ở phía trên đầu nó có 1 loạt các #defines. Từ danh sách này, bạn sẽ được theo dõi bởi SIGSTOP signal. Signal này được gửi khi 1 process nhận 1 "unavoidable suspend instruction". Đây là tín hiệu tương tự với tín hiệu gửi đi khi bạn debug app sử dụng LLDB debugger.

Tới file PhotoCollectionViewController.m và thêm nhũng code sau vào "viewDidLoad" ngay bên dưới [super viewDidLoad]:

- (void)viewDidLoad
{
  [super viewDidLoad];

  // 1
  #if DEBUG
      // 2
      dispatch_queue_t queue = dispatch_get_main_queue();

      // 3
      static dispatch_source_t source = nil;

      // 4
      __typeof(self) __weak weakSelf = self;

      // 5
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
          // 6
          source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGSTOP, 0, queue);

          // 7
          if (source)
          {
              // 8
              dispatch_source_set_event_handler(source, ^{
                  // 9
                  NSLog(@"Hi, I am: %@", weakSelf);
              });
              dispatch_resume(source); // 10
          }
      });
  #endif

  // The other stuff
  1. Chỉ nên biên dịch mã này khi ở chế độ DEBUG vì điều này co thể cung cấp "intersted parties" rất nhiều hiểu biết vào bên trong app của bạn
  2. Chỉ cần trộn lẫn mọi thứ, bạn tạo ra 1 instance của dispatch_queue_t thay vì cung cấp 1 function trực tiếp trong parameter.
  3. sourve cần phải tồn tại bên ngoài scope của method, vì vậy bạn sử dụng static variable.
  4. Bạn sử dụng weakSelf để chắc chắn rằng nó không bị retain. Dù điều này là ko hoàn toàn cần thiết cho PhotoCollectionViewController vì nó vẫn còn alive trong thời gian sống của app. Tuy nhiên, Nếu bạn có bất kỳ class nào mà biến mất, điều này sẽ vẫn đảm bảo không có retain cycles.
  5. Sử dụng "tried and true" dispatch_once để thực hiện thiết lập 1 lần dispatch source.
  6. Ở đây bạn instantiate biến source. Bạn chỉ ra rằng bạn đang quan tâm đến theo dõi tín hiệu và cung cấp SIGSTOP signal như là tham số thứ 2. Ngoài ra, bạn sử dụng main queue để xử lý sự kiện nhận được.
  7. 1 dispatch source object sẽ ko được tạo ra nếu bạn cung cấp các parameter ko chính xác. Kết quả là, bạn phải chắc căhns rằng bạn có 1 đối tượng dispatch source hợp lệ trước khi làm việc với nó.
  8. dispatch_source_set_event_handler gọi khi bạn nhận được signal mà bạn theo dõi. Sau đó bạn đặt các khối xử lý logic thích hợp trong block parameter.
  9. NSLog cơ bản
  10. Mặc định, tất cả các source bắt đầu với suspended state. Bạn phải resume đối tượng source khi bạn muốn bắt đầu theo dõi sự kiện.
0