+3

Grand Central Dispatch in Swift

Grand Central Dispatch (hay GCD) là một trong những công nghệ cơ bản mà hầu hết các Swift developers đã sử dụng rất nhiều . Nó chủ yếu được biết đến vì có thể gửi công việc lên các hàng đợi khác nhau chạy đồng thời với nhau, và hầu hết bạn có thể đã sử dụng nó để viết mã như thế này:

DispatchQueue.main.async {
    // Run async code on the main queue
}

Nếu bạn tìm hiểu sâu hơn, bạn có thể thấy GCD cũng có một bộ các API và tính năng mạnh mẽ mà bạn có thể không biết. Trong bài viết này ta sẽ xem xét một số tình huống mà GCD có thể thực sự hữu ích và làm cách nào để nó có thể cung cấp các tùy chọn đơn giản hơn (và nhiều tính năng hơn) cho nhiều Foundation API khác.

Delaying a cancellable task with DispatchWorkItem

Một quan niệm sai lầm phổ biến về GCD là "khi bạn lập kế hoạch cho một tác vụ, nó không thể bị hủy bỏ, bạn cần sử dụng Operation API cho việc đó". Mặc dù điều này đã từng đúng cho tới khi iOS 8 & macOS 10.10 DispatchWorkItem được giới thiệu, cung cấp chức năng chính xác này trong một API rất dễ sử dụng. Giả sử ta có một search bar, và khi người dùng nhập ký tự và chúng ta thực hiện tìm kiếm bằng cách gọi backend. Vì người dùng có thể gõ khá nhanh, ta không muốn thực hiện search request ngay lập tức (có thể lãng phí tài nguyên của server), thay vào đó ta chỉ gửi request một lần khi người dùng không type trong 0.25 giây. Đây là một trường hợp có thể sử dụng DispatchWorkItem. Bằng cách gộp code vào trong một work item, ta có thể dễ dàng cancel việc request backend cho mỗi sự thay đổi

class SearchViewController: UIViewController, UISearchBarDelegate {
    // We keep track of the pending work item as a property
    private var pendingRequestWorkItem: DispatchWorkItem?

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        // Cancel the currently pending item
        pendingRequestWorkItem?.cancel()

        // Wrap our request in a work item
        let requestWorkItem = DispatchWorkItem { [weak self] in
            self?.resultsLoader.loadResults(forQuery: searchText)
        }

        // Save the new work item and execute it after 250 ms
        pendingRequestWorkItem = requestWorkItem
        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250),
                                      execute: requestWorkItem)
    }
}

Như bạn thấy ở trên, việc sử dụng DispatchWorkItem thực sự đơn giản hơn so với việc sử dụng Timer hoặc Operation. Bạn không cần đánh dấu @objc cho các method, hoặc # selector, tất cả có thể được thực hiện với closures.

Grouping and chaining tasks with DispatchGroup

Đôi khi bạn phải thực hiện một tập các hoạt động trước khi chuyển sang phần logic của bạn. Ví dụ: Giả sử bạn cần phải load data từ một tập các datasource trước khi tạo model. Thay vì tự theo dõi tất cả các nguồn dữ liệu, bạn có thể dễ dàng đồng bộ hoá công việc với một DispatchGroup. Sử dụng dispatch group giúp bạn thuận lợi hơn khi bạn muốn thực hiện task chạy đồng thời trong các hàng đợi riêng lẻ. Điều này cho phép bạn start một cách đơn giản, có thể dễ dàng add thêm các task đồng thời sau đó mà không cần phải viết lại. Tất cả những việc bạn phải làm là gọi enter() và leave() trên dispatch group để đồng bộ các task của bạn. Dưới đây là một ví dụ, ta sẽ load các notes từ local storage, iCloud Drive và backend system, sau đó kết hợp tất cả vào một NoteCollection:

// First, we create a group to synchronize our tasks
let group = DispatchGroup()

// NoteCollection is a thread-safe collection class for storing notes
let collection = NoteCollection()

// The 'enter' method increments the group's task count…
group.enter()
localDataSource.load { notes in
    collection.add(notes)
    // …while the 'leave' methods decrements it
    group.leave()
}

group.enter()
iCloudDataSource.load { notes in
    collection.add(notes)
    group.leave()
}

group.enter()
backendDataSource.load { notes in
    collection.add(notes)
    group.leave()
}

// This closure will be called when the group's task count reaches 0
group.notify(queue: .main) { [weak self] in
    self?.render(collection)
}

Ta sẽ cùng refactor lại code trên với extension của Array

extension Array where Element: DataSource {
    func load(completionHandler: @escaping (NoteCollection) -> Void) {
        let group = DispatchGroup()
        let collection = NoteCollection()

        // De-duplicate the synchronization code by using a loop
        for dataSource in self {
            group.enter()
            dataSource.load { notes in
                collection.add(notes)
                group.leave()
            }
        }

        group.notify(queue: .main) {
            completionHandler(collection)
        }
    }
}

Với phần extension ở trên, bây giờ chúng ta có thể load notes như sau:

let dataSources = [localDataSource, iCloudDataSource, backendDataSource]

dataSources.load { [weak self] collection in
    self?.render(collection)
}

Waiting for asynchronous tasks with DispatchSemaphore

DispatchSemaphore cung cấp một cách để đồng bộ chờ đợi cho một nhóm các task không đồng bộ. Điều này rất hữu ích cho các command line tools hoặc scripts, nơi mà bạn không có vòng chạy của ứng dụng, thay vào đó chỉ là thực hiện đồng bộ trong global context cho đến khi hoàn thành. Giống như DispatchGroup, semaphore API rất đơn giản vì bạn chỉ tăng hoặc giảm một bộ đếm nội bộ bằng cách gọi wait () hoặc signal (). Gọi wait () trước khi signal () sẽ chặn hàng đợi hiện tại cho đến khi tín hiệu được nhận được. Chúng ta hãy tạo ra một function khác trong extension trên Array của ví dụ trước, nó trả về một NoteCollection đồng bộ, hoặc sẽ throws error. Chúng ta sẽ tái sử dụng code dựa trên DispatchGroup từ trước, nhưng chỉ đơn giản là phối hợp công việc đó bằng một semaphore.

extension Array where Element: DataSource {
    func load() throws -> NoteCollection {
        let semaphore = DispatchSemaphore(value: 0)
        var loadedCollection: NoteCollection?

        // We create a new queue to do our work on, since calling wait() on
        // the semaphore will cause it to block the current queue
        let loadingQueue = DispatchQueue.global()

        loadingQueue.async {
            // We extend 'load' to perform its work on a specific queue
            self.load(onQueue: loadingQueue) { collection in
                loadedCollection = collection

                // Once we're done, we signal the semaphore to unblock its queue
                semaphore.signal()
            }
        }

        // Wait with a timeout of 5 seconds
        semaphore.wait(timeout: .now() + 5)

        guard let collection = loadedCollection else {
            throw NoteLoadingError.timedOut
        }

        return collection
    }
}

Sử dụng method trên với Array, bây giờ chúng ta có thể load notes đồng bộ trong script hoặc command line tool như sau:

let dataSources = [localDataSource, iCloudDataSource, backendDataSource]

do {
    let collection = try dataSources.load()
    output(collection)
} catch {
    output(error)
}

Observing changes in a file with DispatchSource

Tính năng ít được biết đến cuối cùng của GCD mà mình muốn đưa ra là cách nó cho phép bạn quan sát những thay đổi trong một file trên file system. Giống như DispatchSemaphore, nó rất hữu ích trong script hoặc command line tool, nếu bạn muốn tự động phản ứng với một file đang được chỉnh sửa bởi user. Điều này cho phép bạn dễ dàng xây dựng các công cụ dành cho nhà phát triển có tính năng "chỉnh sửa trực tiếp". Dispatch sources đi kèm với một vài biến thể khác nhau, tùy thuộc vào những gì bạn đang observe. Trong trường hợp này, chúng ta sẽ sử dụng DispatchSourceFileSystemObject, cho phép chúng ta observe các sự kiện từ file system. Bạn tạo một dispatch source sử dụng fileDescriptor và DispatchQueue để thực hiện observe. Dưới đây là một ví dụ về việc thực hiện một FileObserver đơn giản cho phép bạn chạy một closure mỗi khi một file bị thay đổi:

class FileObserver {
    private let file: File
    private let queue: DispatchQueue
    private var source: DispatchSourceFileSystemObject?

    init(file: File) {
        self.file = file
        self.queue = DispatchQueue(label: "com.myapp.fileObserving")
    }

    func start(closure: @escaping () -> Void) {
        // We can only convert an NSString into a file system representation
        let path = (file.path as NSString)
        let fileSystemRepresentation = path.fileSystemRepresentation

        // Obtain a descriptor from the file system
        let fileDescriptor = open(fileSystemRepresentation, O_EVTONLY)

        // Create our dispatch source
        let source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor,
                                                               eventMask: .write,
                                                               queue: queue)

        // Assign the closure to it, and resume it to start observing
        source.setEventHandler(handler: closure)
        source.resume()
        self.source = source
    }
}

Bây giờ chúng ta có thể sử dụng FileObserver như sau:

let observer = try FileObserver(file: file)

observer.start {
    print("File was changed")
}

Hy vọng bài viết sẽ bổ ích đối với các bạn


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.