Author: Hoang Anh Tuan

  • Grand Central Dispatch (Part 2)

    Grand Central Dispatch (Part 2)

    Nội dung bài viết:

    • Dispatch Group
    • Semaphores

    Dispatch Group (cont):

    Ở phần trước, bạn đã biết về cơ chế enter-leave-notify của DispatchGroup để nhận biết khi các task trong group đã hoàn thành.
    Vậy nếu bạn muốn block luồng hiện tại cho đến khi các tasks trong group hoàn thành hoặc timeout thì sao? Ngoài cách sử dụng 1 vài logic, thì cách đơn giản hơn là dùng phương thức wait do DispatchGroup cung cấp.

    Phương thức wait của DispatchGroup là 1 sync function nên nó có tác dụng block luồng hiện tại cho đến khi mọi tasks trong group đã được thực hiện xong, hoặc timeout.

    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .userInitiated)
    
    var formatter: DateFormatter = DateFormatter()
    formatter.dateFormat = "HH:mm:ss"
    
    // 1
    print("Start task 1 at time: \(formatter.string(from: Date()))")
    queue.async(group: group) {
        Thread.sleep(until: Date().addingTimeInterval(10))
        print("End task 1 at time: \(formatter.string(from: Date()))")
    }
    
    // 2
    print("Start task 2 at time: \(formatter.string(from: Date()))")
    queue.async(group: group) {
        Thread.sleep(until: Date().addingTimeInterval(2))
        print("End task 2 at time: \(formatter.string(from: Date()))")
    }
    
    // 3
    print("Start call time out at time: \(formatter.string(from: Date()))")
    if group.wait(timeout: .now() + 5) == .timedOut {
        print("Time out at time: \(formatter.string(from: Date()))")
    } else {
        print("All the jobs have completed at time: \(formatter.string(from: Date()))")
    }
    
    // 4
    queue.async(group: group) {
        print("Try to do task 3 at time: \(formatter.string(from: Date()))")
    }
    1. Bạn khởi tạo task đầu tiên, đưa vào queue để thực hiện theo cách async.
    2. Bạn khởi tạo task thứ 2, đưa vào queue để thực hiện theo cách async.
    3. Bạn khai báo hàm wait() với timeOut = 5s. Hàm sẽ block luồng hiện tại cho đến khi các task thực hiện xong hoặc sau 5s.
    4. Khởi tạo 1 task thứ 3, đưa vào queue để thực hiện theo cách async
    Mang đoạn code vào playground, thử sửa thời gian để hoàn thành task 1 thành 1s, xem điều gì sẽ xảy ra 😉
    • Sau 2s kể từ lúc bắt đầu, task 2 hoàn thành
    • Khi gọi wait() thì luồng hiện tại sẽ bị block -> Không thực hiện được tiếp task thứ 3 trong khoảng thời gian đó.
    • Sau 5s kể từ lúc Start time out, các task trong group chưa hoàn thành hết do task 1 cần 10s để thực hiện, vì vậy nên dispatchGroup sẽ trả quyền điều khiển về cho luồng.
    • Ngay sau khi luồng được trả quyền điều khiển, luồng thực hiện ngay lập tức task 3.
    • Sau 10s kể từ lúc bắt đầu, task 1 hoàn thành.

    Note: Vì wait() block luồng gọi nó, nên bạn không bao giờ nên gọi wait ở main queue.

    Semaphores

    • Giả sử bạn có nhiều task cùng truy cập vào 1 tài nguyên chung, và bạn muốn giới hạn số lượng task truy cập vào tài nguyên chung đó tại mỗi thời điểm. -> GCD cung cấp cho bạn Semaphore để giúp bạn xử lí việc này dễ dàng.
    • semaphore cung cấp 2 phương thức là wait()signal(). Chúng tương tự như enter() và leave() của dispatchGroup. wait() sẽ gọi khi bắt đầu 1 task và gọi signal() để thông báo rằng task đã hoàn thành.

    Giả sử bạn có 10 task cùng thực hiện 1 lúc, nhưng bạn muốn chỉ có tối đa 4 task chạy tại 1 thời điểm( ví dụ như load ảnh) -> Semaphore sẽ giúp bạn

    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .userInitiated)
    
    // 1
    let semaphore = DispatchSemaphore(value: 4)
    
    // 2
    for i in 1...10 {
        // 3
        queue.async(group: group) {
            // 4
            semaphore.wait()
            // 5
            defer {
                semaphore.signal()
            }
            print(" Start task \(i) at time: \(Date().timeIntervalSince1970)")
    
            Thread.sleep(forTimeInterval: 3)
            print("Finish task \(i) at time: \(Date().timeIntervalSince1970)")
        }
    }
    1. Khởi tạo 1 semaphore, giá trị value biểu thị cho số lượng task tối đa chạy trong 1 thời điểm.
    2. Tạo 1 vòng lặp gồm 10 task chạy.
    3. Mỗi task sẽ chạy theo kiểu async.
    4. gọi hàm wait() để thông báo là task bắt đầu chạy.
    5. gọi signal() để thông báo là task đã hoàn thành.
    Mang đoạn code vào playground, thử không gọi signal() hoặc wait() xem điều gì sẽ xảy ra
    • Task 1, 2, 3, 4 bắt đầu được chạy đầu tiên.
    • Khi task 2, 3, 1 hoàn thành, task 5, 6, 7 ngay lập tức được thực hiện.
    • Task 4 hoàn thành, task 8 ngay lập tức được thực hiện.

      -> Có thể thấy, tại mỗi thời điểm chỉ có tối đa 4 task chạy.

    Note:

    • Luôn nhớ gọi hàm signal() khi task được thực hiện xong, nếu không thì semaphore sẽ không biết task đã hoàn thành để thực hiện task mới.
    • Gọi wait() khi bắt đầu 1 task để thông báo cho semaphore rằng 1 task mới sẽ bắt đầu thực hiện.

    Ở bài viết tiếp theo, mình sẽ giới thiệu về DispatchBarrier, DispatchWorkItem và Thread Sanitizer.

    Nguồn tham khảo: Ray Wenderlich & Lets Build that app.

  • Grand Central Dispatch (Part 1)

    Grand Central Dispatch (Part 1)

    Tổng quan:

    Để app có UI, UX mượt mà và nhanh, ta cần chia các tasks cần nhiều thời gian chạy ra các luồng khác để thực hiện -> GCD là 1 trong các cách giúp các bạn quản lí các luồng đó.

    Nội dung bài viết:

    • 1 số khái niệm cơ bản về luồng, hàng đợi.
    • Cách tạo luồng bằng GCD.
    • Các vấn đề hay gặp phải trong quản lí luồng.
    • Các basic func của GCD.

    Giới thiệu 1 số khái niệm cơ bản về luồng, hàng đợi:

    Thread

    • Luồng là nơi các task được thực hiện.
    • 1 app hoặc 1 chương trình sẽ có 1 hoặc nhiều luồng.
    • Mỗi luồng có thể thực hiện đồng thời, tuy nhiên nó tùy thuộc vào hệ thống để biết khi nào nó được thực hiện và được thực hiện thế nào.
    • GCD được xây dựng dựa trên các luồng. Với GCD, bạn đưa các task vào dispatch queue và GCD sẽ tự quyết định luồng nào được dùng để thực hiện các task.

    Queue

    • Queue là hàng đợi, hoạt động theo cơ chế FIFO.
    • Bạn đưa các task vào queue, và GCD sẽ thực hiện chúng theo cơ chế FIFO.
    • Bản thân Dispatch Queues đã là thread safe nên bạn có thể đưa task vào queue từ bất kì luồng nào.
    • Vì GCD sẽ tự động chọn luồng để thực hiện các task giúp bạn, nên việc của bạn là chỉ cần chọn loại queue phù hợp để đưa task vào.

    Có 2 loại queue là SerialConcurrent.
    Serial: hàng đợi chỉ thực hiện 1 task vào 1 thời điểm, task này xong thì thực hiện task tiếp theo.
    Concurrent: hàng đợi thực hiện nhiều task tại 1 thời điểm, các task vẫn được thực hiện theo thứ tự FIFO, nhưng bạn sẽ không biết số task thực hiện tại 1 thời điểm hay thời điểm các task hoàn thành.

    Serial Queue và Concurrent Queue

    Với GCD, bạn có thể thực hiện các task async hoặc sync.
    sync: 1 task được thực hiện kiểu synchronous sẽ trả quyền điều khiển cho hàm gọi chúng sau khi task được hoàn thành.
    async: 1 task được thực hiện kiểu asynchronus sẽ trả quyền điều khiển ngay lập tức mà không cần đợi phải hoàn thành. Vì vậy, 1 async task sẽ không block luồng hiện tại. Luồng hiện tại sẽ được trả quyền điều khiển để thực hiện task tiếp theo.

    Cách tạo queue với GCD:

    1. Khởi tạo: GCD cung cấp cách khởi tạo luồng với các quality of Service khác nhau như sau:

    Swift gồm 5 loại quality of Service được sử dụng cho các mục đích khác nhau:

    Cách gọi ra Main queue – đây là nơi để bạn thực hiện các task updates UI:

    DispatchQueue.main

    Cách tạo ra 1 private queue: (mặc định là serial queue)

    let serialQueue = DispatchQueue(label: "techOver")

    Để tạo ra 1 queue kiểu concurrent thì bạn set thuộc tính attribute kiểu concurrent:

    let serialQueue = DispatchQueue(label: "techOver", attributes: .concurrent)

    2. Thêm task vào các queue với kiểu thực hiện sync hoặc async:

    DispatchQueue.global().async {
       // do expensive non-UI task
       DispatchQueue.main.async {
          // do updates UI when task is finished
       }
    }

    Các vấn đề hay gặp về quản lí luồng:

    priority inversion: Các task với priority cao sẽ được thực hiện trước các task có priority thấp, tuy nhiên các task có priority thấp lại lock resource, dẫn đến các task với priority cao phải đợi để được thực hiện -> Làm app chậm.
    Race condition: 2 task cùng truy cập để 1 sửa 1 tài nguyên chung trong cùng 1 thời điểm, dẫn đến sai lệch kết quả.

    Deadlock: Xảy ra khi 2 tasks đang đợi lẫn nhau giải phóng tài nguyên để có thể thực hiện tiếp -> Dẫn đến việc cả 2 tasks đều không thể thực hiện -> Treo app.

    Các basic func của GCD:

    1. Dispatch Group: Giả sử bạn có nhiều task được thực hiện theo kiểu async, khi đó bạn sẽ không thể biết khi nào chúng hoàn tất bởi chúng trả về quyền điều khiển ngay lập tức -> Dispatch Group dùng để nhận biết khi 1 nhóm các task được thực hiện xong.

    // 1
    let dispatchGroup = DispatchGroup()
    // 2
    let viewContainer = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
    viewContainer.backgroundColor = .red
    view.addSubview(viewContainer)
    
    // A box move around in the view
    let box = UIView(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
    box.backgroundColor = .yellow
    viewContainer.addSubview(box)
    
    // A label is appear when all the animations finish
    let label = UILabel(frame: CGRect(x: 15, y: 100, width: 360, height: 40))
    label.font = label.font.withSize(50)
    label.text = "All Done!"
    label.textColor = .yellow
    label.textAlignment = .center
    label.isHidden = true
    viewContainer.addSubview(label)
    
    // Animations
    // 3
    dispatchGroup.enter()
    UIView.animate(withDuration: 2, animations: {
        box.center = CGPoint(x: 300, y: 300)
    }, completion: { _ in
        UIView.animate(withDuration: 3, animations: {
            box.transform = CGAffineTransform(rotationAngle: .pi/4)
        }, completion: { _ in
             // 4
             self.dispatchGroup.leave()
        })
    })
    // 5        
    dispatchGroup.notify(queue: .main) {
        UIView.animate(withDuration: 2, animations: {
            viewContainer.backgroundColor = .blue
        }, completion: { _ in
            label.isHidden = false
        })
    }

    Note: Thử đưa đoạn code này vào viewDidLoad và run thử

    1. Khởi tạo 1 DispatchGroup.
    2. Khởi tạo 1 vài view để thực hiện animation.
    3. Gọi hàm enter() để đưa task cần thực hiện vào trong group.
    4. Gọi hàm leave() để báo với group rằng task đã thực hiện xong.
    5. Khai báo xem hàm notify sẽ làm gì khi các task trong group được thực hiện xong. Hàm notify sẽ được tự động gọi đến khi tất cả các task được đưa vào group đã được thực hiện xong.

    Note: Số lần gọi enter() phải bằng số lần gọi hàm leave().

    Kết luận: Thay vì phải đặt delay giữa các animation thì chúng ta có thể dùng DispatchGroup để nhận biết khi 1 animation đã thực hiện xong để thực hiện tiếp animation khác.

    Ở bài viết tiếp theo sẽ tiếp tục là về các basic func và 1 vài advance func của GCD.

  • Swift Generics (Part 2)

    Swift Generics (Part 2)

    Ở phần 1 của bài viết, bạn đã hiểu được Generic là gì, công dụng và cách sử dụng cơ bản của Generic.
    Ở phần 2 của bài viết, bạn sẽ học được:

    • Extending a generic type.
    • Subclass a generic type.
    • Cách sử dụng generic nâng cao.

    Extending a generic type.

    Giả sử bạn có 1 struct Package như sau:

    struct Package<Item> {
        var items = [Item]()
        mutating func push(item: Item) {
            items.append(item)
        }
        
        mutating func popLast() -> Item {
           return items.removeLast()
        }
    }

    Ở đây, Item là 1 generic. Khi bạn extending 1 generic type, bạn không cần khai báo lại <Item>. Item là kiểu dữ liệu generic đã được khai báo trong toàn bộ struct.

    extension Package {
        var firstItem: Item? {
           return items.isEmpty ? nil : items[0]
        }
    }

    extension with a generic where clause

    Giả sử bạn muốn khai báo func isFirstItem(:) như sau:

    Lỗi này tương tự như ở phần 1, khi không phải mọi kiểu dữ liệu trong Swift đều có thể sử dụng toán tử (==) để so sánh. Vậy ở đây, bạn có thể extend generic với 1 mệnh đề where như sau:

    Vì kiểu dữ liệu truyền vào của biến bPerson, mà Person thì không tuân theo Equatable, nên b không có func isFirstItem(:)

    Note: Việc extend 1 generic type với mệnh đề where cho phép bạn thêm 1 điều kiện mới cho extension, vì vậy func isFirstItem(:) chỉ được thêm vào extension khi Item thuộc kiểu Equatable.

    Generic subclass

    Có 2 cách để kế thừa 1 class cha là kiểu generic như sau:

    • Tạo 1 class con kế thừa những thuộc tính và func của class cha nhưng vẫn giữ cho class con là kiểu generic.
    • Tạo 1 class con nhưng là 1 class cụ thể.

    Ở trong ví dụ trên, dễ thấy:

    • Box là một generic subclass của class Package vì bạn muốn chiếc hộp này đựng được bất kì thứ gì, vì vậy Box vẫn là kiểu generic nên khi khởi tạo bạn phải truyền vào một kiểu dữ liệu cụ thể.
    • RosePackage là một subclass cụ thể của class Package, gói RosePackage này sẽ dùng chỉ để đựng hoa hồng.

    Sử dụng generic nâng cao

    Mọi table view cơ bản đều có các func sau:

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
    }
    ...

    Trong trường hợp bạn muốn tạo các tableview khác nhau, thì sẽ dẫn đến việc duplicate các đoạn code này.
    -> Sử dụng generic để giảm thiểu duplicate code.
    Example1:

    RoseTableViewController là 1 subclass cụ thể của 1 generic class.

    Ở đây, bạn sử dụng generic để giảm duplicate code như sau:

    • Tạo 1 generic class tên BaseTableViewController chứa các thuộc tính, func cơ bản, và khai báo 1 placeholder T kiểu UITableViewCell để dễ dàng truyền các kiểu cell khác nhau vào.
    • Tạo 1 class RoseCell kiểu UITableViewCell.
    • Tạo class RoseTableViewController extends BaseTableViewController, class này dùng cell kiểu RoseCell nên chúng ta truyền kiểu RoseCell vào.
    • override lại các thuộc tính -> func numberOfRows tự thay đổi theo.

    Example2 : Trong trường hợp bạn muốn mỗi cell có item kiểu dữ liệu bất kì, bạn có thể khai báo 1 class BaseCell kiểu generic, rồi khởi tạo các cell mới extends BaseCell.

    Note: U ở phần khai báo BaseTableViewController và U ở phần khai báo BaseCell là khác nhau, ở đây mình đặt đều là U cho bạn đọc đỡ confuse.

  • Swift Generics (Part 1)

    Swift Generics (Part 1)

    Generic cho phép bạn viết function và type 1 cách linh hoạt, dễ dàng tái sử dụng, có thể hoạt động với bất kì loại nào tùy theo các yêu cầu mà bạn xác định. Với generic, bạn có thể tránh sự trùng lặp của code mà vẫn thể hiện í định của nó 1 cách rõ ràng.

    Ở phần 1 của bài viết, bạn sẽ học được:

    • Generics là gì ?
    • Tại sao chúng hữu dụng ?
    • Cách sử dụng generic đơn giản.

    Getting Started

    Giả sử bạn được yêu cầu viết 1 phương thức tính tổng 2 số với kiểu dữ liệu bất kì. Thông thường, bạn sẽ viết các func riêng cho các kiểu dữ liệu riêng.

    func addInt(num1: Int, num2: Int) -> Int {
        return num1 + num2
    }
    
    func addDouble(num1: Double, num2: Double) -> Double {
        return num1 + num2
    }

    Dễ dàng nhận thấy, func addInt(::) và func addDouble(::) là 2 func khác nhau nhưng có thân hàm giống nhau. Bạn không chỉ có 2 func, mà code của bạn bị lặp lại.
    -> Generic có thể được sử dụng để giảm 2 hàm này thành 1 và loại bỏ sự trùng lặp code.

    Note

    Trong các func ở trên, kiểu dữ liệu của num1num2 là giống nhau. Nếu kiểu dữ liệu của chúng khác nhau thì không thể cộng 2 số.

    Generic Function

    Generic func có thể hoạt động với bất kì kiểu dữ liệu nào. Dưới đây là code generic của addInt(::) ở trên, tên là addTwoNumbers:

    func addTwoNumbers<T>(num1: T, num2: T) -> T {
       return num1 + num2
    }

    Tưởng tượng bạn có 1 UITextfield, UITextfield đó có 1 trường gọi là placeholder. placeholder sẽ được thay thế bằng text khi bạn nhập text vào UITextfield.
    Tương tự như vậy, func addTwoNumbers(::) sử dụng kiểu dữ liệu T như là 1 placeholder. Kiểu dữ liệu T sẽ được thay thế bằng 1 kiểu dữ liệu xác định do bạn quyết định như Int, Double, … mỗi khi func addTwoNumbers(::) dược gọi. Ta có thể thấy trong func addTwoNumbers(::) ở trên, num1 và num2 có cùng kiểu dữ liệu là T.

    Note:

    • Sự khác biệt giữa generic func và non-generic func là: Ở tên của generic func thì có 1 kiểu dữ liệu placeholder (T) được khai báo trong cặp ngoặc nhọn (<T>). Cặp ngoặc nhọn nói với Swift rằng T là kiểu dữ liệu placeholder.
    • Bạn có thể thay T bằng các các tên khác như A, B, C, Element, … tùy bạn muốn. Việc thay đổi tên chỉ có tác dụng thay đổi tên của kiểu dữ liệu placeholder, chứ không thay đổi gì về tác dụng. Nên tránh đặt tên trùng với các kiểu dữ liệu xác định như Int, String, Double, … để tránh gây nhầm lẫn.
    • Và luôn đặt tên bắt đầu bằng chữ cái in hoa để người đọc hiểu rằng đó là 1 placeholder cho 1 kiểu dữ liệu chứ không phải cho 1 giá trị.

    Tuy nhiên, khi khai báo func addTwoNumbers(::) như trên, bạn sẽ gặp lỗi như sau:

    Lỗi này có nghĩa là, bạn không thể áp dụng toán tử + cho kiểu dữ liệu T vì không phải mọi kiểu dữ liệu trong Swift đều có thể cộng với nhau bằng toán tử (+). Để áp dụng được toán tử (+) cho kiểu dữ liệu T, bạn làm như sau:

    protocol Summable {
        static func +(num1: Self, num2: Self) -> Self
    }
    
    func addTwoNumbers<T: Summable>(num1: T, num2: T) -> T {
        return num1 + num2
    }

    Ở đây, bạn khai báo 1 protocol Summable có chứa toán tử (+) và cho T extends Summable. Khi đó có nghĩa là, kiểu dữ liệu xác định mà bạn muốn đưa vào để thay T thì kiểu dữ liệu đó phải extends Summable, và hiển nhiên là chúng sẽ phải khai báo toán tử (+).

    Sử dụng generic:

    Khi bạn gọi hàm addTwoNumbers, bạn sẽ thay T bằng 1 kiểu dữ liệu xác định. Tuy nhiên, vì T là kiểu Summable, nên kiểu dữ liệu xác định mà bạn muốn truyền vào cũng phải extends Summable và khai báo toán tử + cho kiểu dữ liệu đó.

    Example 1: Gọi hàm addTwoNumbers cho 2 số kiểu Int:

    • Trước hết, phải cho Int extend Summable.
    extension Int: Summable {}

    Note: Vì bản thân các kiểu dữ liệu như Int, Double, String, … đã có chứa sẵn toán tử (+), nên không cần khai báo toán tử (+) cho chúng nữa.

    let number1: Int = 5
    let number2: Int = 10
    let total = addTwoNumbers<Int>(num1: number1, num2: number2) // 15

    Example 2: Khởi tạo và khai báo hàm addTwoNumbers(::) cho 2 số kiểu String :v

    Note: Bạn có thể gọi hàm theo cách: addTwoNumbers<String>(num1: string1, num2: string2), hoặc bỏ <String> đi, vì Swift sẽ dựa vào kiểu dữ liệu bạn truyền vào num1, num2 để tự hiểu kiểu dữ liệu xác định bạn muốn thay cho T là String.

    Ở phần tiếp theo, mình sẽ giới thiệu về:

    • Extending a generic type.
    • Subclass a generic type.
    • Cách sử dụng generic nâng cao.