Lập trình đa nhiệm trên iOS với NSThread, GCD và NSOperation


Bài này tôi sẽ giới thiệu với các bạn những khái niệm cơ bản về lập trình đa nhiệm với NSThread, GCD và NSOperation. Một ứng dụng luôn luôn tồn tại một tiểu trình (thread) chính thực thi ứng dụng đó, việc thực thi bao gồm những xử lý vể giao diện, đọc dữ liệu, tính toán, tất cả code bạn viết mặc định được thực thi trong tiểu trình chính của ứng dụng, còn gọi làmain thread.

Mọi thứ sẽ không vấn đề gì cho đến khi bạn phải thực hiện việc tải nhiều hình từ server, hay phải import dữ liệu từ file CSV hoặc XML có kích thước vài trăm ngàn records để hiển thị, bạn vẫn code được bình thường, và mọi thứ vẫn chạy trên main thread, nhưng lúc này những tương tác người dùng với ứng dụng của bạn không còn mượt mà như trước nữa.  Nguyên nhân là main thread phải xử lý những tác vụ tải, import dữ liệu tốn nhiều thời gian để hoàn thành trước khi kết xuất được kết quả cho người dùng, trong thời gian chờ main thread xử lý xong, người dùng không thể làm gì khác ngoài việc ngồi nhìn ứng dụng của bạn, touch một cách vô vọng, sau đó thì terminate app, Oos oh! No!.

Người dùng không hề muốn touch lên một cái button import để rồi chờ 1-2 giây sau mới thấy kết quả, tôi cũng vậy. Giải pháp cho trường hợp này là bạn phải tạo một tiểu trình (thread) mới, độc lập vớimain thread để thực thi những việc tải hình hay import dữ liệu mất thời gian đó, cho đến khi nào mọi việc thực thi xong, bạn mới đưa dữ liệu đọc được qua main thread để hiện thị kết quảĐể làm được điều này, bạn phải sữ dụng kỹ thuật multithreading bằng NSThread, GCD hoặc NSOperation/NSOperationQueue.

Việc tạo ra một thread mới bằng NSThread khá đơn giản:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1. Tao thread moi de load image
- (void) createThreadToLoadImage
{
   NSString *imageURL = @"http://www.abc.com/example.png";
   NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                                selector:@selector(processLoadingImageInNewThread:)
                                                  object:imageURL];
  [myThread start];
}
// 2. Ham xu ly load image
- (void) processLoadingImageInNewThread:(NSString*) imageURL
{
    NSURL *url = [NSURL urlWithString:imageURL];
    NSData *imageData = [ [NSData alloc] initWithContentsOfURL: url ];
    [self performSelectorOnMainThread:@selector(imageDownloadDidFinish:) withObject:imageData waitUntilDone:NO];
}
// 3. Ham xu ly hien thi image da duoc load xong
- (void) imageDownloadDidFinish:(NSData*) imageData
{
   UIImage *downloadedImage = [UIImage imageWithData:imageData];
   // Hien thi image len image view
   self.imageView.image = downloadedImage;
}

Trong hàm (1), bạn đã tạo ra một thread mới để gọi thực thi hàm xử lý processLoadingImageInNewThread bằng NSThread, khi thread mới được start, nó sẽ chạy song song và bất đồng bộ với main thread, do vậy sẽ không gây ra tình trạng block UI.

Mọi xử lý liên quan đến các thành phần giao diện bắt buộc phải được thực thi trong main thread

Trong hàm (2), xử lý việc load hình từ một remote URL, như tôi đã nói, việc xử lý này đã được đẩy qua một thread khác, không nằm trong main thread,  và một điều quan trọng bạn phải ghi nhớ: “mọi xử lý liên quan đến các thành phần giao diện bắt buộc phải được thực thi trong main thread”, đây là nguyên tắc. Đó là lý do vì sao bạn phải có hàm (3), xử lý việc hiện thị hình đã được load về trong main thread, làm sao bạn biết nó được thực thi trong main thread ? chính vì bạn đã gọi performSelectorOnMainThread trong hàm (2).

Trong hàm (3), như tôi đã nói,  bạn chỉ việc set dữ liệu image lên image view một cách dể dàng. Bạn chú ý, tất cả code xử lý trong hàm (3) bây giờ đang được thực thi trong main thread, không còn trong thread bạn đã tạo ra trước đó nữa.

Đến đây, bạn đã biết cách load một hình từ server bằng việc sử dụng multithreading, đơn giản phải không.

Với kỹ thuật trên,  để load nhiều hình một lúc một cách bất đồng bộ, có bạn đã cải tiến lại hàm (1) như sau:

1
2
3
4
5
6
7
8
9
10
11
// 1A. Tao thread moi de load image
- (void) createThreadToLoadImage:(NSArray*) imageURLs
{
   for( id imageURL in imageURLs)
   {
       NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                                    selector:@selector(processLoadingImageInNewThread:)
                                                      object:imageURL];
       [myThread start];
   }
}

Trong hàm 1A, bạn nghĩ sao nếu mảng imageURLs có 1000 hình,  sẽ có tương ứng 1000 thread được tạo ra. So bad. why ? Bạn nên nhớ số lượng thread luôn tỉ lệ nghịch với năng lực xử lý của CPU, và pin smartphone của bạn. Càng nhiều thread, CPU sẽ càng mệt mỏi, hệ quả là máy sẽ nóng lên, và hao pin rất nhanh. Bạn sẽ làm sao ?

Hãy tưởng tượng mỗi thread là một công nhân làm thuê cho bạn, và bạn phải trả tiền cho điều đó. Giả sử, một công nhân (thread)  thực hiện load 1 hình tốn 1 giây, nếu bạn thuê 1000 công nhân, như vậy sẽ có rất nhiều người ngồi chơi với bạn khi họ làm xong việc. Stupid phải không? tôi biết bạn chỉ thuê 10 công nhân mà thôi, như thế họ sẽ làm việc liên tục một cách hiệu quả cho bạn. Đây chính là cách thức hoạt động của NSOperationQueue.

“Một NSOperationQueue quản lý một danh sách hàng đợi công việc (NSOperation), và một hoặc nhiều thread (công nhân)  để xử lý công việc đó.”

NSOperationQueue tạo ra một hàng đợi, chứa tất cả công việc (NSOperation) mà bạn đưa vào, sau đó phân phát công việc này cho mổi anh công nhân được thuê để xử lý hàng đợi. Thuê bao nhiêu người là do bạn chỉ định. Như vậy, một NSOperationQueue quản lý một danh sách hàng đợi công việc (NSOperation), và và một hoặc nhiều thread (công nhân)  để xử lý công việc đó.

Tất cả những gì bạn phải làm là tạo ra một operation queue, và định nghĩa một công việc (operation) để đưa vào queue. Bạn có thể định nghĩa một công việc bằng cách định nghĩa một lớp XYZOperation kế thừa từ lớp NSOperation, hoặc sữ dụng lớp tiện ích đã được Apple viết sẳn NSInvocationOperation và NSBlockOperation. Dưới đây là một ví dụ sữ dụng NSInvocationOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1B. Load image su dung NSOperationQueue
- (void) usingNSOperationQueueToLoadImage:(NSArray*) imageURLs
{
   NSOperationQueue *operationQueue = [NSOperationQueue new];
   [operationQueue setMaxConcurrentOperationCount:10];
   for( id imageURL in imageURLs)
   {
       NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:
                                             selector:@selector(processLoadingImageInNewThread:)
                                               object:imageURL];
      [operationQueue addOperation:operation];
      [operation release];
   }
   [operationQueue start];
}

Trong 1B, bạn đã tạo ra một operation queue, và chỉ định 10 anh công nhân làm việc cho bạn bằng hàm setMaxConcurrentOperationCount, và tạo ra một danh sách các công việc bằng lớp NSInvocationOperation, bạn lưu ý, bạn đã chỉ định công việc phải làm đơn giản bằng cách set selector cho NSInvocationOperation, đây chính là sự đơn giản mà Apple muốn làm giúp bạn với lớp NSInvocationOperation.

Một điều nữa, bạn không cần quan tâm đến việc tạo và quản lý thread nữa, NSOperationQueue đã làm điều đó giúp bạn, tạo thread, tái sữ dụng lại thread đó cho công việc tiếp theo trong hàng đợi khi một thread đã thực thi xong một công việc trước đó. That is! Đây chính là tiện ích của NSOperationQueue, chính là điều Apple muốn làm giúp bạn, tất cả những gì bạn cần làm là định nghĩa công việc cần phải làm mà thôi.

Bạn vẫn chưa nghe tôi nhắc đến GCD (Grand Central Dispatch), GCD là gì và có giúp gì cho bạn như NSOperationQueue đã làm không ? câu trả lời là Có! nhưng với một cách thức hơi khác thường bằng việc sữ dụng block style coding. Tôi sẽ nói về block style coding trong objective c ở một bài khác, tôi sẽ tập trung vào việc xem GCD hổ trợ việc lập trình multithreading như thế nào. Có 3 vấn đề ta cần quan tâm là hàng đợi công việc, tạo thread và chỉ định hàm xử lý  trong thread với GCD như thế nào.

GCD hỗ trợ việc quản lý một hàng đợi công việc như NSOperationQueue. Mặc định, một ứng dụng iOS luôn luôn tồn tại một hàng đợi được tạo sẵn gọi là main queue, và main thread  được tạo ra để xử lý những công việc trong hàng đợi này. Có một hàng đợi khác gọi là global queue là hàng đợi của hệ thống, quản lý thread đang thực thi trên các ứng dụng khác nhau. Bạn cũng có thể tạo ra hàng đợi của chính bạn bằng GCD.

Bạn không phải chỉ định có bao nhiêu thread được phép khởi tạo trong một hàng đợi, điều này dể hiểu đối với main queue và global queue, nhưng ngay cả hàng đợi do chính bạn tạo ra cũng vậy. Việc tạo ra bao nhiêu thread là hiệu quả nhất, iOS sẽ chỉ định điều đó giúp bạn.

Như vậy việc còn lại bạn quan tâm là tạo ra một công việc để GCD đưa vào hàng đợi như thế nào, và làm sao để lấy dữ liệu kết quả khi công việc trong hàng đợi của GCD được xử lý xong. Tôi sẽ cho bạn xem một ví dụ cách thức làm điều đó với GCD:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1C. Load image su dung GCD
- (void) usingGCDToLoadImage:(NSArray*) imageURLs
{
   // C0. Code dang duoc thuc thi tren main thread
   // Tao ra mot hang doi bang GCD
   dispatch_queue_t myQueue = dispatch_queue_create("com.myApp.myQueue", NULL);
   // Dua danh sach cong viec vao hang doi
   for( id imageURL in imageURLs)
   {
      dispatch_async(myQueue,
      ^{
         // C1. Code dang duoc thuc thi trong thread cua hang doi do ban tao ra, khong phai la main thread
         NSURL *url = [NSURL urlWithString:imageURL];
         NSData *imageData = [ [NSData alloc] initWithContentsOfURL: url ];
         // Dua ket qua ve main thread de xu ly hien thi UI
         dispatch_async(dispatch_get_main_queue(),
         ^{
              // C2. Code dang duoc thuc thi trong main thread
              UIImage *downloadedImage = [UIImage imageWithData:imageData];
              self.imageView.image = downloadedImage;
          });
       }
}

Trong 1C, đầu tiên bạo tạo 1 hàng đợi của chính bạn như cách bạn tạo ra một NSOperatioQueue dùng hàm dispatch_queue_create. Tiếp theo  bạn đưa danh sách công việc vào hàng đợi này.

Bạn để ý hàm dispatch_async(…), hàm này được truyền vào 2 tham số, tham số đầu tiên là queue mà bạn đã tạo ra ở bước 1, tham số thứ 2 là một hàm xử lý được định nghĩa dưới dạng block, một block xử lý được định nghĩa trong dấu ^{…}, hơi khác thường nếu bạn chưa làm việc với block bao giờ, nhưng đừng quan tâm nó. Bạn chỉ cần hiểu, bạn đang truyền vào tham số cho hàmdispatch_async là một hàm xử lý (hay đúng hơn một con trỏ hàm), hàm này sẽ được triệu gọi trong xử lý của hàm dispatch_async.

Bạn chú ý, tùy vào queue mà bạn truyền vào hàm dispatch_async mà đoạn code block xử lý ở tham số thứ 2 sẽ chạy trên những thread khác nhau, nếu bạn truyền vào main queue thì code xử lý của bạn sẽ chạy trên main thread, và ngược lại.

Ở đoạn code trên, đầu tiên bạn đã truyền vào 1 queue do chính bạn định nghĩa, nên GDC sẽ tự tạo quản lý (tạo hoặc tái sử dụng) 1 thread khác không phải là main thread để thực thi đoạn code C1 của bạn. Khi C1 được xử lý xong, kết quả phải được đưa về main thread để hiện thị. Như tôi đã nói, bạn chỉ việc gọi lại hàm dispatch_async và đưa vào main queue, bạn lấy main queue của ứng dụng bằng hàm dispatch_get_main_queue được hổ trợ bởi GCD, như vậy đoạn code C2 của bạn sẽ được thực thi trên main thread.

Trong một hàm usingGCDToLoadImage nhưng lại có 2 đoạn code khác nhau chạy trên 2 thread khác nhau. Đây chính là điểm khác thường khi bạn sữ dụng GCD với block style coding, nhưng tôi chắc đến đây bạn đã hiểu vì sao, và bây giờ đó là điều bình thường thôi.

Về tính năng thì GCD và NSOperationQueue có chung một mục đích, từ iOS 4 trở về trước, thì GCD và NSOperationQueue được implement độc lập nhau, nhưng từ iOS 4 trở về sau này, NSOperationQueue được implement lại dựa trên GCD.

Chắc chắn bây giờ, bạn sẽ muốn quên đi NSThread, và lựa chọn sữ dụng NSOperationQueue hoặc GCD. Điều đó là tùy thuộc vào bạn, nhưng hãy suy nghĩ xem tại sao bạn dùng GCD mà không dùng NSOperationQueue và ngược lại. Suy nghĩ nhé. Tôi phân tích cùng bạn ở bài sau.

Have a good day !

Advertisements

Về haipro912
Đời rất dở nhưng anh vẫn phải niềm nở =))

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

w

Connecting to %s

%d bloggers like this: