+5

Async await: từ cơ bản đến nâng cao trong Swift

Qua bài này các bạn sẽ dễ dàng nắm được cách sử dụng từ cơ bản đến nâng cao của asyn, await trong lập trình concurrency để tối ưu hoá hiệu năng và đảm bảo sự an toàn dữ liệu khi xử lý nhiều tác vụ đồng thời


Giới thiệu Async & await

Swift 5.5 đã giới thiệu mô hình đồng thời (concurrency model) mới, tạo ra một bước tiến lớn trong việc làm cho lập trình bất đồng bộ trở nên dễ dàng và an toàn hơn. Hai từ khóa async và await giúp lập trình viên xử lý mã bất đồng bộ một cách gọn gàng và có cấu trúc hơn.

Trước đây, khi làm việc với các tác vụ bất đồng bộ, lập trình viên thường phải sử dụng các kỹ thuật như callbacks, completion handlers, hoặc closures. Những phương pháp này thường khiến mã trở nên phức tạp và khó theo dõi, đặc biệt khi có nhiều cấp độ lồng nhau (callback hell). Với sự ra đời của async và await, Swift đã cung cấp một giải pháp đơn giản hơn, giúp giảm thiểu độ phức tạp và cải thiện khả năng đọc hiểu mã.

  • Async: Khi một hàm được khai báo là async, điều đó có nghĩa là hàm này sẽ thực hiện một tác vụ có thể mất một khoảng thời gian để hoàn thành (ví dụ: gọi API, xử lý dữ liệu lớn). Tuy nhiên, hàm này sẽ không chặn main thread, cho phép các tác vụ khác tiếp tục chạy trong khi chờ tác vụ bất đồng bộ hoàn thành.
  • Await: Từ khóa await được sử dụng để đợi kết quả từ một tác vụ bất đồng bộ. Khi một tác vụ được đánh dấu với từ khóa await, luồng thực thi sẽ tạm dừng tại đó cho đến khi tác vụ đó hoàn thành. Khi hoàn thành, luồng sẽ tiếp tục chạy các câu lệnh tiếp theo. Điều này giúp mã trở nên tuyến tính hơn và dễ hiểu hơn so với cách sử dụng callbacks trước đây.

Lợi ích của async/await so với callback:

  • Tuyến tính: Trước khi có async/await, lập trình viên thường phải sử dụng callbacks hoặc completion handlers, điều này khiến mã trở nên phức tạp và khó theo dõi. Async/await giúp mã trông giống như đồng bộ, dễ đọc hơn.
  • Quản lý lỗi: Sử dụng từ khóa try/catch giúp bạn có thể xử lý lỗi một cách rõ ràng và hiệu quả.

Ví dụ cơ bản

Dưới đây là ví dụ cơ bản về cách sử dụng Swift's concurrency model với từ khóa async và await để lấy dữ liệu từ một URL

import Foundation
// (1) 
func fetchData(from url: String) async throws -> Data {
    guard let url = URL(string: url) else {
        throw URLError(.badURL)
    }
    // (2)
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}
// (3)
Task {
    do {
        // (4)
        let data = try await fetchData(from: "https://example.com")
        print("Data fetched: \(data)")
    } catch {
        print("Failed to fetch data: \(error)")
    }
}

Cách hoạt động:

  • (1) Từ khoá async cho phép hàm thực hiện 1 tác vụ mất thời gian mà không chặn main thread.
  • (2) Từ khoá await tạm dừng việc thực thi hàm này cho đến khi tác vụ tải dữ liệu từ URL hoàn thành.
  • (3) Phần chạy tác vụ: tạo 1 Task block, nơi chúng ta có thể gọi các hàm bất đồng bộ trong một block code {}. Task được sử dụng để chạy code async từ bất kỳ đâu trong ứng dụng, đặc biệt khi không thể sử dụng async ở hàm cấp cao hơn.
  • (4): Kết hợp tryawait để gọi hàm bất đồng bộ fetchData và bắt lỗi nếu có.

Advanced Usage Scenarios

Load Concurrent Data

Sử dụng keyword async let để khởi tạo và chạy nhiều tác vụ bất đồng bộ cùng lúc. Sau đó, chờ tất cả các tác vụ hoàn thành bằng cách sử dụng keyword await để xử lý kết quả của chúng cùng nhau

import Foundation
func fetchMultipleData() async {
    // (1)
    async let data1 = fetchData(from: "https://example.com/1")
    async let data2 = fetchData(from: "https://example.com/2")
    async let data3 = fetchData(from: "https://example.com/3")
    
    do {
         // (2)
        let results = try await (data1, data2, data3)
        print("Fetched data: \(results)")
    } catch {
        print("Error fetching data: \(error)")
    }
}
Task {
    // (3)
    await fetchMultipleData()
}

Cách hoạt động

  • (1): async let được sử dụng để khởi tạo một tác vụ bất đồng bộ. Ở đây, hàm fetchData (mà chúng ta đã tạo ở ví dụ trước) được gọi để tải dữ liệu từ URL. Tương tự, async let data2async let data3 cũng khởi tạo hai tác vụ khác để lấy dữ liệu từ các URL tương ứng.
  • (2): keyword await được sử dụng để chờ kết quả của tất cả các tác vụ. Điều này đảm bảo rằng khi mã tiến hành đến đây, cả ba tác vụ data1, data2, và data3 đều đã hoàn thành và trả về kết quả.
  • (3): Task block để gọi hàm fetchMultipleData().

Xử lý Timeouts and Cancellation

Việc đảm bảo rằng một tác vụ không chạy vô hạn và có thể bị hủy khi cần thiết là rất quan trọng, đặc biệt là khi làm việc với các yêu cầu mạng.

import Foundation

func fetchData(from url: String) async throws -> Data {
    let url = URL(string: url)!
    // (1)
    let (data, _) = try await URLSession.shared.data(from: url)
    
    // Check for cancellation
    try Task.checkCancellation()
    
    return data
}
// (3)
func fetchDataWithTimeout(from url: String) async throws -> Data {
    // (4)
    let task = Task {
        try await fetchData(from: url)
    }
    // (5)
    let timeoutTask = Task {
        try await Task.sleep(nanoseconds: 5_000_000_000)
        task.cancel()
    }
    
    do {
        // (6)
        return try await task.value
    } catch {
        // (7)
        timeoutTask.cancel()
        throw error
    }
}
// (8)
Task {
    do {
        let data = try await fetchDataWithTimeout(from: "https://example.com")
        print("Data fetched: \(data)")
    } catch {
        print("Operation timed out or was cancelled: \(error)")
    }
}

Cách hoạt động:

  • (1) Sử dụng URLSession thực hiện request với method GET đến URL và trả về response dạng Data.
  • (2) Kiểm tra xem tác vụ hiện tại có bị yêu cầu hủy bỏ (cancel) hay không. Nếu tác vụ đã bị hủy, nó sẽ ném ra một lỗi CancellationError. Việc kiểm tra hủy bỏ trong quá trình thực hiện các tác vụ dài hạn đảm bảo rằng tác vụ được giải phóng tài nguyên đúng cách.
  • (3) Hàm này thực hiện việc gọi fetchData(from:) nhưng có thêm logic để xử lý timeout (giới hạn thời gian thực hiện). Nó bao gồm hai tác vụ song song:
    • (4) Tác vụ chính để tải dữ liệu từ URL.
    • (5) Tác vụ phụ là một timeout task, thực hiện một bộ đếm (sleep) kéo dài 5 giây. Sau khi 5 giây trôi qua, nó sẽ gọi task.cancel() để hủy bỏ tác vụ chính nếu nó vẫn chưa hoàn thành.
  • (6): Đợi tác vụ chính hoàn thành bằng cách gọi await task.value. Nếu tác vụ hoàn tất thành công trước khi hết thời gian, kết quả sẽ được trả về.
  • (7): Nếu tác vụ chính thất bại hoặc bị hủy, khối catch sẽ bắt lỗi. Ở đây, ta cũng hủy bỏ timeoutTask để tránh lãng phí tài nguyên không cần thiết.
  • (8): Tạo một tác vụ để chạy hàm fetchDataWithTimeout(). Nếu hàm hoàn thành thành công trước khi hết thời gian, dữ liệu sẽ được in ra. Ngược lại, nếu tác vụ bị hủy hoặc quá thời gian, thông báo lỗi sẽ xuất hiện.

Sử dụng Task Groups

Task Group để quản lý và thực thi nhiều tác vụ bất đồng bộ cùng lúc, cho phép khởi tạo, quản lý và kiểm soát các tác vụ chạy bất đồng bộ. Cụ thể sử dụng withThrowingTaskGroup để quản lý các tác vụ chạy bất đồng bộ và có khả năng throws ra lỗi khi có sự cố xảy ra.

import Foundation
func fetchMultipleDataWithGroup(urls: [String]) async throws -> [Data] {
    // (1)
    return try await withThrowingTaskGroup(of: Data.self) { group in
        for url in urls {
            // (2)
            group.addTask {
                try await fetchData(from: url)
            }
        }
        
        var results: [Data] = []
        for try await result in group {
            // (3)
            results.append(result)
        }
        return results
    }
}
// (4)
Task {
    do {
        let urls = ["https://example.com/1", "https://example.com/2", "https://example.com/3"]
        let data = try await fetchMultipleDataWithGroup(urls: urls)
        print("Fetched data: \(data)")
    } catch {
        print("Error fetching data: \(error)")
    }
}

Cách hoạt động:

  • (1) Hàm withThrowingTaskGroup để tạo một nhóm các tác vụ chạy song song và có thể throws lỗi khi bất kì hàm nào xảy ra lỗi.
  • (2) Thêm một tác vụ chạy song song mới vào group.
  • (3) Mỗi khi một tác vụ hoàn thành, kết quả sẽ được thêm vào mảng results.
  • (4) TạoTask block được tạo để gọi hàm fetchMultipleDataWithGroup. Mảng urls chứa các URL cần tải dữ liệu. Nếu việc tải dữ liệu thành công, nó sẽ in ra kết quả ở mảng data. Nếu có lỗi xảy ra (ví dụ như không kết nối được mạng hoặc URL không hợp lệ), lỗi sẽ được catch và in ra error.

Quản lý Dependency

Để xử lý các tác vụ phụ thuộc lẫn nhau, async await giúp viết các tác vụ asynchronous giống như luồng thực thi code synchronous, giúp dễ đọc và dễ maintain.

import Foundation
func fetchAndProcessData() async throws -> String {
    async let data1 = fetchData(from: "https://example.com/1")
    async let data2 = fetchData(from: "https://example.com/2")
    
    let result1 = try await data1
    let result2 = try await data2
    
    // Process the data
    let processedData = "Processed: \(result1) and \(result2)"
    return processedData
}
Task {
    do {
        let result = try await fetchAndProcessData()
        print("Result: \(result)")
    } catch {
        print("Error: \(error)")
    }
}

Source code mô tả quá trình tải dữ liệu từ hai URL khác nhau song song, sau đó xử lý kết quả của cả hai dữ liệu cùng lúc khi chúng hoàn tất, cho phép các tác vụ chạy đồng thời và kết hợp kết quả của chúng một cách hiệu quả.

Kiểm tra kết nối Internet

Trước khi thực hiện bất kỳ yêu cầu mạng nào, điều quan trọng là phải kiểm tra kết nối internet. Đây là cách bạn có thể tích hợp kiểm tra reachability trong các request network của mình.

Trong ví dụ này, lớp Reachability sẽ kiểm tra kết nối Internet trước khi thực hiện request.

import Foundation
import SystemConfiguration

class Reachability {
    static func isConnectedToNetwork() -> Bool {
        var zeroAddress = sockaddr_in()
        zeroAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
        zeroAddress.sin_family = sa_family_t(AF_INET)
        
        let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) {
            $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { zeroSockAddress in
                SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress)
            }
        }
        
        var flags = SCNetworkReachabilityFlags()
        if !SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) {
            return false
        }
        
        let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0
        let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0
        return (isReachable && !needsConnection)
    }
}
func fetchDataIfConnected(from url: String) async throws -> Data {
    guard Reachability.isConnectedToNetwork() else {
        throw URLError(.notConnectedToInternet)
    }
    return try await fetchData(from: url)
}
Task {
    do {
        let data = try await fetchDataIfConnected(from: "https://example.com")
        print("Data fetched: \(data)")
    } catch {
        print("Failed to fetch data: \(error)")
    }
}

Tổng kết

Mô hình concurrency của Swift với keyword async và await thực sự là công cụ mạnh mẽ để viết asynchronous code gọn gàng, dễ đọc, và hiệu quả. Dù bạn đang xử lý những tác vụ bất đồng bộ đơn giản hay trong mô hình phức tạp, cách tiếp cận này cung cấp sự linh hoạt và an toàn để quản lý các tác vụ song song một cách hiệu quả.

Khi bạn kết hợp các tình huống nâng cao như:

  • Tải dữ liệu đồng thời từ nhiều nguồn,
  • Xử lý giới hạn thời gian (timeouts),
  • Sử dụng task groups để quản lý nhiều tác vụ,
  • Xử lý các phụ thuộc giữa các tác vụ,
  • Và kiểm tra kết nối mạng,...

Hãy bắt đầu khám phá những kỹ thuật nâng cao này trong các dự án của bạn và nâng tầm kỹ năng lập trình bất đồng bộ của bạn lên một cấp độ mới. See you!

Tài liệu tham khảo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí