+1

Grand Central Dispatch - Part 2

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

Ta đang thảo luận về vấn Singleton không phải là thread-safe ở phần trước, tiếp theo, để khiến cho race condition xảy ra, ta viết code sharedManager trong PhotoManager.m như sau:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    if (!sharedPhotoManager) {
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager = [[PhotoManager alloc] init];
        NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    }
    return sharedPhotoManager;
}

Ở hía trên, chúng ta đã cố tình khiến cho 1 context switch xảy ra với 1 class method: NSThread's sleepForTimeInterval

Mở file AppDelegate.m và thêm những dòng code sau vào ngay trên đầu bên trong hàm application:didFinishLaunhingWithOptions:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [PhotoManager sharedManager];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [PhotoManager sharedManager];
});

Nó tạo 2 nhiều câu lệnh đồng thời và ko đồng bộ cùng tạo ra thực thể singleton mới và dẫn đến race condition như đã nói ở trên. Sau khi chạy chương trình, check phần console output bạn có thể thấy nhiều singletons được tạo ra như dưới đây:

NSLog-Race-Condition-700x90.png

Chúng ta có thể thấy những dòng này đều hiển thị các dịa chỉ khác nhau của các singleton instance. Điều này làm sụp đổ hoàn toàn mục đích sử dụng của singleton. Output cho ta thấy một critical section bị thực hiện nhiều lần khi mà nó chỉ đc phép thực hiện 1 lần. Mặc dù ở đây, ta đã cố ý để tình huống này xảy ra, tuy nhiên bạn cũng có thể tưởng tượng đc rằng tính huống này là hoàn toàn ko khó khăn để vô tình xảy ra.

Để sửa chữa tình trạng này, các code tạo ra instance chỉ được thực hiện 1 lần và ngăn chặn tất cả các hoạt động khác muốn tạo ra instance khi mà nó đang ở trong critical section của điều kiện if. Đây chính xác là những gì dispatch_once thực hiện. THay thế câu lệnh if với dispatch_once trong phương pháp khởi tạo singleton như sau:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager = [[PhotoManager alloc] init];
        NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    });
    return sharedPhotoManager;
}

Run app, check phần console và bạn sẽ thấy chỉ có duy nhất 1 instance của singleton được tạo ra. Giờ chúng ta đã hiểu tầm quan trọng của việc ngăn chặn race conditions xảy ra, bỏ phần dispatch_async trong AppDelegate.m đi và thay thế đoạn code init singleton PhotoManager như sau:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedPhotoManager = [[PhotoManager alloc] init];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    });
    return sharedPhotoManager;
}

dispatch_once() thực hiện khối lệnh chỉ 1 lần và 1 lần duy nhất với ý nghĩa thread safe. Các threads khác nhay khi mà cố găgns truy cập vào critical section - code sẽ được chuyển tới dispatch_once - khi 1 thread đã ở trong, section sẽ được blocked cho đến khi critical section hoàn thành công việc.

Cần chú ý rằng, điều này chỉ làm cho các shared instance trở nên thread safe. Nó ko thể làm 1 class trở thành thread safe. Bạn vẫn có các critical sections trong class, để instance mọi thứ mà thao túng dữ liệu nội bộ. Những điều này cần phải được thread safe theo những cách khác nhau, chẳng hạn như đồng bộ truy cập tới dữ liệu, như chúng ta bàn luận trong phần sau đây.

Handling the Readers and Writers Problem:

Khởi tạo kiểu thread-safe không phải là vấn đề duy nhất khi làm việc với singleton. Nếu Property của singleton là 1 mutable object, bạn cần phải xem xét xem object đó bản thân nó có phải là thread-safe hay không. Nếu như object là 1 class thuộc Foundation container class, thì câu trả lời sẽ là "có lẽ không"! Apple duy trì 1 danh sách gồm các class thuộc Foundation mà không phải là thread-safe. ví dụ như NSMutableArray. Mặc dù có thể nhiều thread đọc 1 NSMutableArray cùng 1 lúc mà không có vấn đề gì, nó là không an toàn khi để 1 thread thay đổi array trong khi 1 thread khác đọc nó. Ở thời điểm này, singleton của bạn không hề ngăn chặn tình trạng này xảy ra. Để thấy được vấn đề, hãy xem method addPhoto: trong Photomanager.m:

- (void)addPhoto:(Photo *)photo
{
    if (photo) {
        [_photosArray addObject:photo];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self postContentAddedNotification];
        });
    }
}

Đây là 1 method kiểu write: thay đổi 1 đối tượng mảng mutable và private. Giờ hãy xem photos:

- (NSArray *)photos
{
  return [NSArray arrayWithArray:_photosArray];
}

Đây là một method kiểu read làm nhiệm vụ đọc 1 mutable array. Nó tạo ra 1 bản copy không thể thay đổi được của array để chống lại việc nhiều người thay đổi nó cùng 1 lúc, nhưng không có ai trong số này cung cấp 1 biện pháp chống lại việc 1 thread gọi đến method addPhoto: trong khi đồng thời 1 thread khác gọi đến method read photo.

Đây là Readers-Writers Problem cổ điển trong phát triện phần mềm. GCD cung cấp 1 giải pháp thanh lịch bằng cách tạo ra 1 cái khoá (lock) Readers-Writer sử dụng dispatch barriers.

Dispatch barriers là 1 nhóm các function hoạt động như một serial-style bottleneck khi làm việc với concurrent queues. Sử dụng API của GCD's barrier đảm bảo rằng block đc submit là item duy nhất được thực hiện trên queue đã chỉ định ở thời gian cụ thể đó. Điều này có nghĩa là tất cả các item được gửi đến queue và chờ trước dispatch barrier phải hoàn thiện trước khi block được thực hiện. Khi đến lượt của block, barrier sẽ cho block thực hiện và chắc chắn rằng queue không thực hiện bất kỳ 1 block nào trong thời điểm này. Sau khi hoàn tất, queue đó sẽ trở về thực hiện mặc định của nó. GCD cung cấp cả 2 chức năng: synchronous and asynchronous barrier functions

Biểu đồ dưới đây minh hoạc tác động của các barrier functions trên các block không đồng bộ khác nhau:

Dispatch-Barrier.png

Chú ý rằng trong hoạt động bình thường, queue hoạt động giống như 1 concurrent queue bình thường. Nhưng khi barrier được thực hiện, nó sẽ thực hiện giống như serial queue. Có nghĩa là: barrier là thứ duy nhất được thực hiện. Sau khi barrier kết thúc, queue sẽ trở lại thành concurrent queue. Hướng dẫn sử dụng đơn giản về barrier functions như sau:

  • Custom Serial Queue: rõ ràng là 1 lựa chọn tồi, barrier sẽ chẳng thể làm gì có ích trong 1 serial queue
  • Global Concurrent Queue: Sử dụng thận trọng, bởi các hệ thống khác có thể sử dụng những queue này và bạn không muốn đọc chiến chúng cho mục địch của riêng mình
  • Custom Concurrent Queue: Đây rõ ràng là lựa chọn tuyệt vời cho các "atomic or critical areas of code." Bất cứ thứ gì mà bạn setting hoặc instantiating mà cần phải thread safe là 1 ứng việc tuyệt hảo cho barrier.

Khi mà lựa chọn phong nhã duy nhất ở trên là custom concurrent queue, bạn sẽ cần phải tạo ra 1 queue riêng của mình để xử lý barrier function và tách 2 function read và write riêng nhau. Các concurrent queue sẽ cho phép nhiều read operations cùng 1 lúc.

Mở PhotoManager.m và thêm 1 property vào phần category của class:

@interface PhotoManager ()
@property (nonatomic,strong,readonly) NSMutableArray *photosArray;
@property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue; ///< Add this
@end

Thay đổi hàm addPhoto: như sau:

- (void)addPhoto:(Photo *)photo
{
    if (photo) { // 1
        dispatch_barrier_async(self.concurrentPhotoQueue, ^{ // 2
            [_photosArray addObject:photo]; // 3
            dispatch_async(dispatch_get_main_queue(), ^{ // 4
                [self postContentAddedNotification];
            });
        });
    }
}

function mới hoạt động như sau:

  • Check xem photo có hợp lệ ko trước khi xử lý nó
  • thêm phép toán write ([_photosArray addObject:photo]) vào custom queue . Khi critical section xảy ra sau đó, sẽ chỉ còn duy nhất 1 item trong queue để xử lý
  • đây là code thực sự add object vào array. Vì nó là barrier block, nên nó sẽ không bao giờ chạy đồng thời với bất kỳ block còn lại noà trong concurrentPhotoQueue.
  • Cuối cùng, bạn post a notification mà bạn add vào image. Notification này có thể được posted từ main thread bởi vì nó làm công việc của UI, vì thế ở đây bạn gọi 1 task khác không đồng bộ tới main queue cho notification

Điều này quan tâm đến hàm Write, nhưng bạn cũng cần phải implement hàm read phots và instantiate concurrentPhotoQueue. Để chắc chắn vấn đề write là thread safe, bạn cần phải thực hiện việc đọc trên concurrentPhotoQueue. Bạn cần phải return từ function, nên bạn không thể gọi bất đồng bộ đến queue bởi vì không nhất thiết phải chạy trước khi read functions return.

Trong trường hợp này, dispatch_sync sẽ là 1 lựa chọn xuất sắc. dispatch_sync() submits công việc 1 cách đồng bộ và chờ cho nó hoàn thành trước khi return. Sử dụng dispatch_sync để theo dõi công việc với dispatch barries, hoặc khi bạn cần phải đợi 1 công việc nào đó hoàn thành trước khi có thể sử dụng data được xử lý bởi block. Nếu bạn làm việc ở trường hợp thứ 2, đôi khi bạn sẽ thấy 1 biến __block viết bên ngnoaif phạm vi dispatch_sync để sự dụng các object đã được xử lý trả về bên ngoài dispatch_sync.

Bạn cần phải cẩn thận. Tưởng tượng bạn gọi dispatch_sync và nhắm tới queue hiện tại mà bạn đang chạy trên đó. Điều này sẽ gây ra deadlock vì the call sẽ đợi cho đến khi block hoàn thành, nhưng block không thể kết thúc (nó thậm chí còn ko thể start nổi) trừ khi task đang được thực hiện kết thúc, mà điều đó là không thể được. Điều này sẽ buộc bạn phải có ý thức trong việc queue nào sẽ được bạn gọi cũng như là queue nào bạn đang xử lý ở trên.

Tổng quan về veiecj sử dụng dispatch_sync khi nào và ở đâu:

  • Custom Serial Queue: hết sức cẩn thận trong tình huống này, nếu bạn đang chạy trên 1 queue và gọi dispatch_sync nhắm tới cùng queue đó, bạn chắc chắn sẽ tạo ra deadlock.
  • Main Queue (Serial): tương tự như trên, hết sức cẩn thận.
  • Concurrent Queue: đây là 1 trường hợp tốt để đồng bộ hoá công việc thông qua dispatch barries hoặc khi chờ 1 task hoàn thành, dó đó bạn có thể thực hiện các xử lý xa hơn.

Tiếp tục làm việc với PhotoManager.m:

- (NSArray *)photos
{
    __block NSArray *array; // 1
    dispatch_sync(self.concurrentPhotoQueue, ^{ // 2
        array = [NSArray arrayWithArray:_photosArray]; // 3
    });
    return array;
}

Đây là 1 read function, hoạt động như sau:

  • Từ khoá __block cho phép các đối tượng có thể thay đổi khi ở bên trong 1 block. Nếu không có điều này, code của bạn thậm chí còn không thể compile được.
  • Gọi đồng bộ tới concurrentPhotoQueue để thực hiện việc read
  • Lưu mảng photo vào trong biến array và return nó

Cuối cùng, bạn cần phải instantiate concurrentPhotoQueue, thay đổi sharedManager để instantiate queue như sau:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedPhotoManager = [[PhotoManager alloc] init];
        sharedPhotoManager->_photosArray = [NSMutableArray array];

        // ADD THIS:
        sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue",
                                                    DISPATCH_QUEUE_CONCURRENT);
    });

    return sharedPhotoManager;
}

Việc khởi tạo concurrentPhotoQueue như một concurrent queue được thực hiện bởi dispatch_queue_create. Parameter đầu tiên là cách đặt tên theo kiểu DNS, phải chắc chắn rằng nó có tính gợi vì sẽ giúp ích nhiều chp bạn khi debug. Parameter thứ 2 quyết định việc queue ciuar bạn là concurrent hay serial. Như vậy là singleton của PhotoManager đã hoàn toàn là thread-safe. Bạn ko còn lo lắng về việc read và write photo ở đâu hay ntn nữa.

Đánh gía trực quan về Queueing:

Nếu bạn vẫn cảm thấy không chắc chắn 100% về các yếu tố cần thiết của GCD, hãy tạo 1 project ví dụ, sử dụng break point và NSLog để hiểu đc những gì xảy ra. Ở đây có 2 ảnh gif có thể giúp bạn hiểu về dispatch_async và dispatch_sync:

dispatch_sync Revisited:

- (void)viewDidLoad
{
  [super viewDidLoad];

  dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

      NSLog(@"First Log");

  });

  NSLog(@"Second Log");
}

dispatch_sync_in_action.gif

  • main queue thực hiện các nhiệm vụ theo thứ tự, trong đó task tiếp theo là instantiate UIViewController (bao gồm cả method viewDidLoad)
  • viewDidLoad thực thi trên main thread
  • main thread đang ở trong viewDidLoad và sắp tới dispatch_sync.
  • block dispatch_sync được thêm vào global queue và sẽ được thực hiện sau đó. Mọi xử lý đang dừng lại ở main thread cho đến khi block hoàn thành coong việc. Trong khi đó, global queue đồng thời xử lý các nhiệm vụ, nhớ rằng các block đc dequeued theo thứ tự FIFO trên một global queue nhưng có thể đc thực hiện đồng thời.
  • Global queue xử lý các task đã có trước khi khối dispatch_sync được add.
  • dispatch_sync đuwocj thực hiện.
  • Block kết thúc và các task trên main thread có thể tiếp tục
  • viewDidLoad done, main thread tiếp tục thưucj hiện các nhiệm vụ khác.

dispatch_async Revisited

- (void)viewDidLoad
{
  [super viewDidLoad];

  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

      NSLog(@"First Log");

  });

  NSLog(@"Second Log");
}

dispatch_async_in_action.gif

  • main queue thực hiện các nhiệm vụ theo thứ tự, trong đó task tiếp theo là instantiate UIViewController (bao gồm cả method viewDidLoad)
  • viewDidLoad thực thi trên main thread
  • main thread đang ở trong viewDidLoad và sắp tới dispatch_async.
  • block dispatch_async được thêm vào global queue và sẽ được thực hiện sau đó.
  • viewDidLoad tiếp tục công việc sau khi add dispatch_async vào global queue và main thread chú ý vào các task còn lại. Trogn khi đó, global queue đồng thời thực hiện xử lý các công việc của mình.nhớ rằng các block đc dequeued theo thứ tự FIFO trên một global queue nhưng có thể đc thực hiện đồng thời.
  • các code được thêm vào bởi dispatch_async bây h được thực hiện.
  • block dispatch_async haonf tất và cả 2 câu lệnh NSLog đã in ra console.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí