Grand Central Dispatch - Part 1
Bài đăng này đã không được cập nhật trong 3 năm
Tài liệu tham khảo từ: http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1
- Grand Central Dispatch (GCD) là gì: GCD là libdispatch - 1 thư viện của Apple hỗ trợ cho việc xử lý đồng thời trên các phần cứng đa lõi (multicore) trên iOS và OS X. Nó cung cấp các lợi ích sau:
- GCD có thể cải thiện sự phản hồi (responsiveness) của app bằng cách tạm trì hoãn các task nặng nề và chạy ngầm chúng dưới background.
- GCD cung cấp một mô hình xử lý đồng thời (concurrency model) dễ dàng hơn so với "locks & thread" và giúp tránh các lỗi xảy ra khi xử lý đồng thời.
- GCD có khả năng tối ưu hoá code với hiệu suất cao hơn cho các pattern phổ biến như singletons.
- 1 Số thuật ngữ đi kèm với GCD:
- Serial vs. Concurrent: tuần tự và đồng thời: 2 thuật ngữ này mô tả về việc các tasks khi thực hiện liên quan đến nhau ntn. Những tasks thực hiện tuần tự luôn luôn được thực hiện theo kiểu mỗi lần 1 task: trong 1 thời điểm chỉ có 1 task được thực hiện (one at a time), còn các task đồng thời thì có thể được thực hiện cùng 1 lúc. Trong phạm vi bài này ta có thể coi 1 tasl mjiw ;à 1 block trong objective-C.
- Synchronous vs. Asynchronous: đồng bộ và bất đồng bộ: 2 thuật ngữ này mô tả việc 1 function khi hoàn tất công việc của nó thì liên quan đến các task khác - mà nó yêu cầu GCD xử lý - như thế nào. 1 function đồng bộ chủ trả về khi task mà nó yêu cầu đã đuược hoàn thành. Còn function ko đồng bộ thì trả về ngay lập tức, nó vẫn yêu cầu task phải được thực hiện nhưng ko chờ đợi task đó làm xong. Vì vậy, function bất đồng bộ sẽ không block thread hiện tại *
- Critical Section: Là đoạn code mà không được phép thực hiện đồng thời (từ 2 thread cùng 1 lúc). Đây thường là do các đoạn code này thao tác trên 1 nguồn tài nguyên dùng chung - như 1 biến có tểh bị hỏng nếu nó được truy xuất bởi các process đồng thời.
- Race Condition: đây là 1 hoàn cảnh khi mà "hành vi" của 1 hệ thống phần mềm phụ thuộc vào 1 trình tự cụ thể hoặc thời điểm của các events mà được thực hiện 1 cách không kiểm soát được, ví dụ như thứ tự chính xác của các lệnh thực thi của 1 chương trình chứa các task đồng thời. Race condition có thể dẫn đến các hành động không thể đoán trước mà không hề thể hiện ra thông qua kiểm tra mã...
- Deadlock: 2 (hoặc nhiều hơn) threads được coi là deadlocked nếu chúng mắc kẹt trong việc chờ đợi lẫn nhau hoàn thành để thực thi các hành động khác. Task đầu ko thể kết thúc vì chờ đợi Task sau kết thúc, nhưng task sau cũng ko thể hoàn thành nếu task đầu chưa hoàn thành.
- Thread Safe: "Thread Safe code" có thể được gọi 1 cách an toàn từ các multiple threads hoặc concurrent tasks mà ko gây ra bất kỳ vấn đề nào (data corruption, crashing, etc...). Code mà không phải là thread safe thì chỉ được chạy trong 1 context tại 1 thời điểm. 1 vd về thread safe là NSDictionary. Bạn có thể sử dụng nó tại cùng 1 thời điểm từ nhiều thread khác nhay mà ko vấn đề gì cả. Mặt khác, NSMutableDictionary ko phải là thread safe và chỉ đc truy cập từ 1 thread tại 1 thời điểm.
- Context Switch: là 1 quá trình lưu trữ và không phục lại trạng thái thực hiện (execution state) khi bạn chuyển đổi việc thực hiện giữa các thread khác nhau trong cùng 1 process. Kiểu process này khá là phổ biến khi viết các ứng dụng multitasking, nhưng đi cùng với nó là là 1 chi phí bổ sung khá lớn.
- Concurrency vs Parallelism: đồng thời và song song. Concurrentcy và parallelism thường được nhắc đến cùng nhau, vì vậy cần phải phân biệt bởi chúng ko hẳn là một. Các phần riêng biệt của concurrent code có thể được thực hiện cùng 1 lúc ("simultaneously"). Tuy nhiên, điều đó còn tuỳ thuộc vào hệ thống quyết định khi nào thực hiện điều này.Các thiết bị multi-core thực hiện nhiều thread cùng 1 lúc thông qua parallelism; tuy nhiên để cho các thiết bị single-core có thể đạt được điều này , chúng phải chạy 1 thread, thực hiện 1 context switch, rồi chạy 1 thread hoặc 1 process khác. Điều này thường xảy ra đủ nhanh để tạo ra cảm giác rằng các công việc được thực hiện 1 cách song song như sơ đồ dưới đây:
Mặc dù bạn có thể viết code để sử dụng các tính toán đồng thời bên dưới GCD, nó vẫn phụ thuộc vào GCD để quyết định bao nhiêu song song là cần thiết. Song song thì đòi hỏi phải "đồng thời", còn đồng thời ko hề đảm bảo các xử lý là song song(như hình vẽ). Vấn đề sâu hơn ở đây đó là concurrency thực sự là 1 khái niệm về cấu trúc. Khi bạn thiết kế code với GCD, bạn phải cấu trúc code của mình để bộc lộ rõ những phần có thể chạy đồng thời cũng như những phần ko thể.
- Queues: hàng đợi GCD cung cấp các dispatch queues để xử lý các khối code, những queue này quản lý các task mà bạn đẩy đến cho GCD và thực hiện theo thứ tự FIFO: Nó đảm bảo task đầu tiên được thêm vào queue là task đầu tiên sẽ được thực hiện, task thứ hai sẽ được thực hiện thứ hai và cứ tiếp tục thế. Tất cả các dispatch queues là thread-safe trong đó bạn có thể truy cập chúng từ nhiều thread cùng 1 lúc. Lợi ích của GCD hết sức rõ ràng khi bạn hiểu làm ntn mà dispath queues cung cấp thread-safe cho 1 phần code của bạn. Vấn đề ở đây đó là chọn đúng loại dispatch queue và đúng dispatching function để submit công việc của bạn tớ queue. Ở phần này, bạn sẽ xem qua 2 loại dispatch queues, các queues đặc biệt mà GCD giới thiệu, và làm việc thông qua 1 ví dụ minh hoạ: add công việc vào queues với GCD dispatching functions.
- Serial Queues: Task trong serial queue thì tại 1 thời điểm chỉ có 1 task được thực hiện, mỗi task chỉ bắt đầu sau khi task trước đã hoàn thành. Đồng thời, bạn cũng sẽ không thể biết được khoảng thời gian giữa: 1 task kết thúc và task tiếp theo bắt đầu, như được thể hiện dưới hình sau:
Thời gian tính toán của các task này nằm dưới sự kiểm soát của GCD, thứ duy nhất bạn đảm bảo đc biết đó là GCD chỉ thực hiện duy nhất 1 task tại 1 thời điểm và nó thực hiện các task theo thứ tự được thêm vào hàng đợi. Bởi vì không bao giờ có 2 task trong cùng 1 serial queue có thể chạy đồng thời nên sẽ ko có rủi ro khi chúng cùng truy cập vào 1 critical section cùng 1 lúc.
- Concurrent Queues: Task trong concurrent queues đươc đảm bảo rằng chúng sẽ được chạy theo đúng thứ tự mà chúng đã được add vào. Và các task có thể kết thúc theo bất cứ thứ tự nào mà bạn cũng ko hề biết được thời gian chênh lệc giữa 2 task bắt đầu, hoặc số lượng task chạy trong cùng 1 thời điểm nào đó. Điều này hoàn toàn phụ thuộc vào GCD. Biểu đồ dưới đây sẽ cho bạn thấy 1 cách rõ ràng:
Chúng ta có thể thấy rằng: Block1, 2 và 3 chạy rất gần nhau (vẫn đúng thứ tự khởi chạy) trong khi khoảng cách giữa Block 0 và 1 là rất dài. Ngoài ra, Block 3 bắt đầu sau Block 2 nhưng lại hoàn thành trước. Việc quyết định block chạy lúc nào hoàn toàn do GCD. Nếu thời gian thực hiện của 2 khối trùng lặp với nhau, điều đó phụ thuộc vào GCD tính toán sẽ cho 1 khối chạy trên 1 core khác, hoặc thực hiện 1 context switch để chuyển sang 1 block code khác. GCD cugn cấp cho bạn 5 loại queue đặc biệt để lựa chọn.
- Queue Types: Đầu tiên, hệ thống cung cấp cho bạn 1 serial queue đặc biệt là "main queue". Nó đảm bảo tất cacr các task đều được thực hiện trên main thread, đó là thread duy nhất cho phép update giao diện của bạn. Main queue được dùng để gửi mesages tớ UIViews hoặc post notifications. Hệ thống cũng cung cấp cho bạn 1 số concurrent queues: Global Dispatch Queues. Hiện tạo có 4 global queue với các mức ưu tiên khác nhau: background, low, default, and high. Bạn phải chú ý rằng Apple's APIs cũng sử dụng các queues này nên những task bạn add vào không phải là những task duy nhất trên các queues này. Cuối cùng, bạn cũng có thể tạo ra các custom serial or concurrent queues. Như vậy bạn có ít nhất 5 queue để sử dụng: main queue, 4 flobal dispatch queues, cộng với bất kỳ kiểu custom queue nào. "Nghệ thuật" của GCD đó là chọn lựa đúng queue dispatching function để đưa công việc của bạn vào đó. Cách tốt nhất để trải nghiệm là thông qua ví dụ dưới đây:
Chúng ta sẽ bắt đầu với 1 project gần hoàn thiện là GooglePuff Sau khi download project và run với Xcode ta sẽ thấy app như bên dưới:
Chú ý rằng khi bạn chọn "Le Internet" để download ảnh, 1 cái pop-up UIAlertView sẽ bật lên trước khi ảnh đc download về hết. Chúng sẽ fix trong phần 2 của bài này.
Có 4 class cần quan tâm trong project này:
- PhotoCollectionViewController: Đây là View Controller (VC) đầu tiên - bắt đầu app. Nó show ra tất cacr các ảnh đã được chọn.
- PhotoDetailViewController: Xử lý logic và thêm các mắt "googly" cho nhân vật trong ảnh và hiển thị kết quả trong 1 UIScrollView.
- Photo:
- PhotoManager: quản lý tất cả các instance của Photo.
-
Xử lý background task với dispatch_sync
bây h, bạn hãy sử dụng app và add thêm photo từ Photo Library hoặc sử dụng option "Le Internet" để download. Bạn có thể thấy ngay rằng có 1 khoảng lag nhỏ khi instantiate 1 PhotoCollectionViewController sau khi click vào 1 UICollectionViewCell trong PhotoCollectionViewController, đặc biệt khi view những ảnh dung lượng lớn trên những device yếu. Hàm viewDidLoad của UIViewController rất dễ bị quá tải với quá nhiều sự lộn xộn, nó thường gây ra việc load view chậm (chờ đợi lâu trước khi view controller xuất hiện). Nếu có thể, tốt nhất là giảm tải những công việc cần thực hiện bằng cách xử lý trong background nếu nó ko hoàn toàn cần thiết trong thời gian tải, và đó là công việc cho dispatch_async.
Mở PhotoDetailViewController và thay thế hàm viewDidLoad với:
- (void)viewDidLoad
{
[super viewDidLoad];
NSAssert(_image, @"Image not set; required to use view controller");
self.photoImageView.image = _image;
//Resize if neccessary to ensure it's not pixelated
if (_image.size.height <= self.photoImageView.bounds.size.height &&
_image.size.width <= self.photoImageView.bounds.size.width) {
[self.photoImageView setContentMode:UIViewContentModeCenter];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1
UIImage *overlayImage = [self faceOverlayImageFromImage:_image];
dispatch_async(dispatch_get_main_queue(), ^{ // 2
[self fadeInNewImage:overlayImage]; // 3
});
});
}
Đây là những gì xảy ra ở trên:
-
1: Trước tiên, bạn chuyển công việc ra khỏi main thread và đưa nó vào 1 global queue. Bởi vì đây là 1 dispatch_asynch(), block bên trong sẽ được submit ko đồng bộ có nghĩa là công việc của thread hiện tại vẫn tiếp tục. Điều này cho phép hàm viewDidLoad kết thúc sớm hơn trên main thread và làm cho việc loading nhẹ nhàng linh hoạt hơn. Trong khi đó, việc xử lý nhận diện khuôn mặt được bắt đầu và sẽ hoàn thành sau đó.
-
2: Tại thời điểm này, Việc xử lý nhận diện khuôn mặt được hoàn tất và bạn đã tạo ra 1 hình ảnh mới. Và vì bạn muốn sử dụng cái ảnh mới này để update lại UIImageView, bạn add 1 block mới vào trong main queue. Hãy nhớ rằng bạn phải luôn luôn access các class của UIKit trong main thread
-
3: Cuối cùng bạn cập nhập UI với hàm fadeInNewImage: thực hiện 1 fade-in transition cho hình ảnh mới với googly eyes.
Build&Run app, lựa chọn 1 hình ảnh và bạn sẽ nhận thấy rằng View Controller load nhanh hơn đáng kể, đồng thời add "googly eyes" vào trong 1 khoảng delay nhỏ. Điều này giúp cho app có 1 hiệu ứng khá đẹp: bạn show được bức ảnh trước và sau khi add "googly eyes" với ấn tượng tốt nhất. Tương tự, nếu bạn thử tải 1 bức ảnh với dung lượng cực lớn, View Controller sẽ ko bị treo ở process load view controller.
Như đã đề cập ở trên, dispatch_async gắn thêm 1 block vào 1 queue và return ngay lập tức. Cái task đó sẽ được thực hiện tại 1 thời gian sau - theo sự quyết định của GCD. Sử dụng dispatch_async khi bạn cần phải thực hiện 1 nhiệm vụ dựa trên network hoặc CPU trên background và ko làm block thread hiện tại.
Custom Serial Queue: 1 lựa chọn tốt khi bạn muốn thực hiện công việc trên background 1 cách tuần tự và muốn theo dõi nó. Điều này giúp loại bỏ việc tranh chấp tài nguyên vì bạn biết rõ chỉ có 1 task được thực hiện tại 1 thời điểm. Lưu ý rằng nếu bạn cần dữ liệu từ 1 method, bạn cần phải inline 1 block khác để lấy về hoặc xem xét, sử dụng dispatch_async.
Main Queue(Serial): Đây là lựa chọn phổ biến để update UI sau khi hoàn thành công việc trong 1 task ở trên 1 concurrent queue. Để làm điều này, bạn sẽ phải code 1 block bên trong 1 block khác. Cũng như vậy, bạn đang ở trong main queue và gọi dispatch_async nhắc vào main queue, bạn có thể đảm bảo rằng task mới này sẽ thực hiện lúc nào đó khi method hiện tại đã kết thúc.
Concurrent Queue: đây là lựa chọn phổ biến để xử lý các công việc non-UI (ko liên quan đến giao diện ) trong background.
Delaying Work with dispatch_after
Hãy xem xét về vấn đề UX của app này 1 chút. Có thể dễ dàng nhận ra rằng user sẽ bị bối rối về việc phải làm gì khi mở app lần đầu tiên. Tốt nhất, ta nên có 1 dấu nhắc cho user nếu như ko có ảnh trong Photomanager class. Tuy nhiên, bạn cần phải nghĩ về cách mà mắt người dùng điều hướng trong màn hình chủ (home screen): nếu bạn hiển thị 1 dấu nhắc quá nhanh, họ sẽ bỏ lỡ . Trước khi hiển thị prompt chúng ta nên để chờ 1s, như vậy là vừa đủ để gây ra sự chú ý cho người dùng khi họ lần đầu sử dụng app.
Add code sau vào showOrHideNavPromt trong PhotoCollectionViewController.m:
-(void)showOrHideNavPrompt
{
NSUInteger count = [[PhotoManager sharedManager] photos].count;
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); // 1
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2
if (!count) {
[self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"];
} else {
[self.navigationItem setPrompt:nil];
}
});
}
Method showOrHideNavPrompt được thực hiện trong hàm viewDidLoad bất cứ lúc nào UICollectionView được reload. Mỗi 1 comment số ở trên có ý nghĩa như sau:
- Khai báo 1 biến xác định lượng thời gian delay
- Sau đó bạn chờ đúng khoảng thời gian delayInSeconds để add block vào main queue 1 cách bất đồng bộ.
Build&Run app, và sẽ có 1 độ trễ nhỏ trc khi hiển thị prompt với mong muốn gây chú ý đến người dùng và cho họ thấy phải làm gì. dispatch_after hoạt động giống như 1 dispatch_async bị delayed. bạn vẫn ko thể kiểm soát thời điểm thực tế của execution này và cũng ko thể huỷ bỏ khi mà dispatch_after return.
Khi nào phù hợp để sử dụng dispatch_after:
- Custom Serial Queue: Hết sức cẩn thận khi sử dụng dispatch_after trên 1 hàng đợi tuỳ chỉnh nối tiếp. Bạn nên gắn nó trên main queue.
- Main Queue(Serial): Đây là 1 lựa chọn tốt cho dispatch_after; Xcode có 1 autocomplete template rất đẹp cho loại này.
- Concurrent Queue: Cần cẩn thận khi sử dụng loại này trên custom concurent queues; hiếm khi nào mà ta sử dụng nó.
Making your Singletons Thread-Safe
1 mối lo lắng về singetons đó là nó ko phải thread-safe. Mối lo ngại này là chính đáng bở việc sử dụng chúng: singletons thường đc sử dụng từ nhiều controllers, truy cập tới instance của singletons cùng 1 lúc. Bắt đầu từ lúc init cho tới đọc và ghi thông tin với singletons. Class PhotoManager được implement như 1 singleton - và nó cũng vướng phải những vấn đề này. Để thấy ta có thể sai lầm như thế nào, hãy cùng tạo 1 "race condition" trên singleton này.
Mở PhotoManager.m và tìm sharedManager, code sẽ như sau:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
if (!sharedPhotoManager) {
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
}
return sharedPhotoManager;
}
Code phía trên ko có gì ngoài tạo ra 1 singleton và tạo ra 1 property private kiểu NSMutableArray với tên là photosArray.
Tuy nhiên, câu lệnh if ko phải là thread-safe. Nếu bạn gọi method này nhiều lần cùng lúc, sẽ có khả năng 1 thread (tạm gọi là thread-A) có thể sẽ vào đc block của if và 1 "context switch" có thể sẽ xảy ra trước khi sharedPhotoManager được cấp phát bộ nhớ (Allocated). Và rồi 1 thread khác (Thread-B) có thể vào đc block if, allocate 1 instance của singleton và thoát ra.
Khi hệ thôgs context quay trở lại với Thread-A, bạn sẽ allocate 1 instance khác của singleton và thoát ra. Thời điểm này bạn sẽ có 2 instance của singeton - và đó ko còn ý nghĩa của singleton nữa.
All rights reserved