iOS concurrency

Concurrency luôn được coi là một chủ đề khó trong quá trình phát triển iOS, các dev thường hay cố gắng hết mình để tránh. Tôi có thể đồng ý với các bạn rằng concurrency rất khó nếu bạn không hiểu. Nhưng nếu bạn hiểu thì nó lại là một vũ khí lợi hại cho bạn viết code đó. Concurrency thực sự là một vũ khí hai mặt, nó có thể giúp ứng dụng bạn hoạt động tốt nhưng nó cũng có thể phá huỷ ứng dụng của bạn một cách không thương tiếc.

Tại sao chúng ta cần sử dụng concurrency?

  • Sử dụng phần cứng của thiết bị: Ngày nay hầu hết các thiết bị đều cho phép sử dụng multi core để thực hiện các nhiệm vụ song song
  • Trải nghiệm người dùng tốt hơn: Nếu ứng dụng của bạn mọi thứ đều xử lý trên main thread thì ứng dụng của bạn sẽ rất hay bị treo và không thể đáp ứng lại hay phản hồi lại các tương tác người dùng.
  • Dễ dàng sử dụng API: Việc tạo và quản lý thread không phải là điều dễ dàng, đây chính là lý do vì sao khi nhắc tới concurrency và multi thread nhiều dev đều lo lắng và sợ hãi. Nhưng trong iOS cung cấp api cho bạn để sử dụng một cách dễ dàng.

Chúng ta cần biết gì về concurrency?

GCD: grand central dispatch

GCD là api thường dùng để quản lý mã và thực hiện các hoạt động không đồng bộ cấp hệ thống, cung cấp và quản lý hàng đợi của nhiệm vụ, vậy hàng đợi là gì?

a. Hàng đợi là gì?

Queue

b. Dispatch queue

  • Thực hiện nhiệm vụ không đồng thời cho ứng dụng của bạn, dạng block
  • Có 2 loại dispatch queue: serial queuesconcurrency queues.
  • Trước khi nói về sự khác nhau thì chúng ta nên hiểu nó có điểm giống nhau đó là khi task được giao cho hai dispatch queue này thì cả hai task sẽ được thực hiện trên khác (thread được tạo ra từ disaptch queue). Nói cách khác thì khối mã bạn viết được tạo ra từ thread chính nhưng thực hiện trên một thread khác Serial queue: Khi bạn tạo ra một serial queue thì tại một thời điểm chỉ có 1 task được thực hiện, các task cứ lần lượt thực hết sau khi task trước đó kết thúc. Tuy nhiên đó là góc nhìn 1 serial queue, bạn vẫn có thể tạo ra các serial queue khác, như vậy tại một thời điểm thì vẫn có thể có nhiều task được thực hiện đồng thời, nhưng khi đó bạn cần phải quan tâm tới việc kết quả thực hiện của hai task trong hai serial queue đó có liên quan tới nhau hay không. • Lợi thế khi sử dụng serial queue - Đảm bảo tuần tự khi truy cập tới 1 tài nguyên - Thứ tự thực hiện đoán được - Có thể tạo ra bất kỳ số lượng nào cho hàng đợi nối tiếp đó Concurrency queue: cho phép thực hiện các nhiệm vụ song song, nhiệm vụ bắt đầu theo thứ tự chúng được thêm vào hàng đợi, việc thực hiện thì xảy ra đồng thời không phải chờ nhau, bạn sẽ không biết thời gian thực hiện, số lượng công việc và thời điểm thực hiện công việc.

c. Sử dụng queues

  • Bên trên tôi đã giải thích hai loại hàng đợi bên trên là dispatch và serial. Quan trọng hơn là làm thế nào để sử dụng chúng. Theo mặc định hệ thống cung cấp một serial queue và 4 concurrency queue.
    • Main dispatch queue (serial queue) là toàn cục với nhiệm vụ thực hiện task liên quan tới update UI view và chỉ có một nhiệm vụ được thực hiện tại một thời điểm, đây chính là lý do vì sao khi dev cho thực hiện 1 nhiệm vụ nặng nề trên main thread -> thực hiện lâu và dẫn tới lâu phản hồi lại tương tác người dùng.
    • Còn lại với 4 concurrency queue đã nói bên trên sẽ có một vài giá trị tham chiếu với mức độ ưu tiên lần lượt từ trên xuống dưới như sau: DISPATCH_QUEUE_PRIORITY_HIGH DISPATCH_QUEUE_PRIORITY_DEFAULT DISPATCH_QUEUE_PRIORITY_LOW DISPATCH_QUEUE_PRIORITY_BACKGROUND

Sample code

1. Sử dụng concurrency dispatch queues

@IBAction func didClickOnStart(sender: AnyObject) {
    let img1 = Downloader.downloadImageWithURL(imageURLs[0])
    self.imageView1.image = img1
    
    let img2 = Downloader.downloadImageWithURL(imageURLs[1])
    self.imageView2.image = img2
    
    let img3 = Downloader.downloadImageWithURL(imageURLs[2])
    self.imageView3.image = img3
    
    let img4 = Downloader.downloadImageWithURL(imageURLs[3])
    self.imageView4.image = img4
    
}
  • Mỗi downloader được xem như một task và khi thực hiện xong quay lại main queue và update
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
        dispatch_async(queue) { () -> Void in
            
            let img1 = Downloader.downloadImageWithURL(imageURLs[0])
            dispatch_async(dispatch_get_main_queue(), {
                
                self.imageView1.image = img1
            })
            
        }

2. Sử dụng serial dispatch queue

@IBAction func didClickOnStart(sender: AnyObject) {
    
    let serialQueue = dispatch_queue_create("com.appcoda.imagesQueue", DISPATCH_QUEUE_SERIAL)
   
    dispatch_async(serialQueue) { () -> Void in
        
        let img1 = Downloader .downloadImageWithURL(imageURLs[0])
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView1.image = img1
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView2.image = img2
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView3.image = img3
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView4.image = img4
        })
    }
    
}

Note: Với hai cách viết code cho hai loại queue các bạn cũng có thể hình dung ra cách thực hiện các task

Ngoài ra mình muốn nói thêm một chút về operation queues

  • Operation queue thực chất nó không khác gì GCD cả, nó được trang bị như mô hình lập trình hướng đối tượng thôi, nó vẫn thực hiện các nhiệm vụ đồng thời. Nhưng cũng có một vài điểm khác với GCD như sau:
    • Không theo FIFO: bạn có thể thay đổi độ ưu tiên các task
    • Bạn có thể thay đổi ràng buộc giữa các task
    • Được đóng gói thành NSOperation cho tương ứng mỗi task
  • NSOperation là lớp trừu tượng nên khi sử dụng bạn phải subclass, bên cạnh đó có 2 lớp bạn có thể sử dụng trực tiếp
    • NSBlockOperation
    • NSInvocationOperation
  • Bạn có thể thay đổi độ ưu tiên
public enum NSOperationQueuePriority : Int {
    case VeryLow
    case Low
    case Normal
    case High
    case VeryHigh
}

- Cách sử dụng

@IBAction func didClickOnStart(sender: AnyObject) {
    queue = NSOperationQueue()
 
    queue.addOperationWithBlock { () -> Void in
        
        let img1 = Downloader.downloadImageWithURL(imageURLs[0])
 
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView1.image = img1
        })
    }
    
    queue.addOperationWithBlock { () -> Void in
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView2.image = img2
        })
 
    }
    
    queue.addOperationWithBlock { () -> Void in
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView3.image = img3
        })
 
    }
    
    queue.addOperationWithBlock { () -> Void in
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView4.image = img4
        })
 
    }
}

- Các bạn có thể thêm phụ thuộc ví dụ như op2 phụ thuộc op1, op3 phục thuộc op2

operation2.addDependency(operation1) operation3.addDependency(operation2)

**- Cancel: ** Khi bạn thêm phụ thuộc như vậy, muốn huỷ bạn chỉ cần cancelAllOperations()

Tổng kết: Trên đây mình đã phần nào giúp các bạn hiểu về concurrency và đặc biệt cách sử dụng nó trong iOS.