HomeOur Team
Part 1 - Cơ bản về Async/ Await

Part 1 - Cơ bản về Async/ Await

By phuong.bui
Published in Solutions
November 11, 2022
5 min read

Giới thiệu

Ở sự kiện WWDC21 năm ngoái, Apple đã có sự cập nhật mới về các hàm bất đồng bộ trong swift 5.5. Đó là sự xuất hiện của asyncawait.

A/e dev C# hay JS nghe đến đây thì kiểu: 😏 😏 😏 meme1.webp

Quả đúng là async/await là những keyword chẳng xa lạ gì đối với anh em lập trình, nhưng cập nhật mới này của Apple, đối với dev iOS lại mang ý nghĩa khá to lớn. Nó đã cho thấy sự tập trung và ưu tiên phát triển của Concurency, thu hẹp đáng kể về khoảng cách cũng như thân thiện hơn đối với các dev trong việc xử lý Concurency.

Nói đến đây thì nhiều anh em lại tò mò muốn biết async/await có gì hay ho, cơ mà khoan, trước khi tìm hiểu về async/await thì t sẽ nhắc lại về Completion Handle

Các vấn đề với Completion Handle

Completion Handle chắc hẳn đã quá là quen thuộc với mỗi dev iOS. Anh em dev iOS sử dụng nhiều hơn cả ăn cơm mỗi ngày. Tuy nhiên, dùng nhiều quen tay, không ít dev lại chẳng nhận ra những nhược điểm của nó. Cùng phân tích ví dụ đơn giản dưới đây để thấy rằng Completion Handle cũng không hoàn hảo như chúng ta tưởng :D

Bài toán

Bạn muốn lấy 1 image từ 1 Url, sau đó chuyển image đó về 1 ảnh thumbnail bé hơn và hiển thị hình ảnh thumbnail đó ra ngoài view, trường hợp bị lỗi sẽ update view để người dùng có thể biết dc. Ok, bài toán đã có, giờ chúng ta sẽ tiến hành triển khai bài toán theo cách mà trước giờ chúng ta đều làm. Let’s go!

Triển khai

Từ yêu cầu của bài toán, việc chúng ta cần làm đầu tiên là thực hiện việc lấy ảnh từ URL. Tất nhiên, việc lấy ảnh này sẽ không thể có kết quả ngay, và để tránh block luồng chính, thì việc download ảnh sẽ là 1 xử lý bất đồng bộ, và khi request hoàn thành, chúng ta sẽ sử dụng Completion Handle để pass dữ liệu về - như cách mà chúng ta vẫn thường làm. Func cơ bản sẽ như sau..

func fetchThumbnailImage(from urlString: String, completion: @escaping (UIImage?, Error?) -> Void)
{
guard let url = URL(string: urlString) else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(nil, error)
} else {
guard let data = data else {
return
}
let image = UIImage(data: data)
// xử lý lấy ảnh thumbnail....
}
}
task.resume()
}

OK, ở trên là một đoạn code xử lý việc lấy ảnh từ URL, tuy nhiên, đã có 1 chút vấn đề ở đoạn code trên. Hãy nhìn lại xem liệu b có nhận ra ngay lập tức vấn đề nằm ở đâu không? (trước khi kéo xuống dưới để xem kết quả ) 😆😆😆

… … …

Và vấn đề của đoạn code trên, nó nằm ở đây.

fetch_thumbnail_image_function_error

Đúng vậy, ở đây t đã return và thoát ra khỏi func luôn mà k gọi completion.

T đoán là không chỉ mình t, mà kha khá các bạn dev iOS khác, đặc biệt là các bạn fresher, sau khi viết guard let ... là lập tức return ngay mà chẳng màng gì khác, nào biết đâu, User vẫn đang chờ đợi request hoàn thành mãi mà chẳng view cập nhật gì. 😅 => Và đó là vấn đề đầu tiên khi sử dụng Completion Handle: Quên không gọi hoặc gọi quá nhiều completion

Được rồi, giờ t sẽ chỉnh sửa lại code một chút và thêm xử lý ảnh thumbnail. Thật may là swift cung cấp một func có sẵn cho việc này và chúng ta chỉ việc gọi nó ra. Và code hoàn chỉnh cho cả bài toán sẽ như thế này:

completion_handle_full_code

Ở đây, việc xử lý trả về ảnh thumbnail mà swift cung cấp, cũng là 1 xử lý bất đồng bộ, vì vậy ta sẽ phải sử dụng thêm 1 function với Completion Handle.

Đây chỉ là một bài toán đơn giản, nhưng đã phải sử dụng liên tiếp 2 function với Completion Handle, mục đích chỉ là chờ xử lý này xong rồi mới đến xử lý tiếp theo. Trong thực tế, nhiều khi không chỉ có 2 function lồng nhau như thế này. Việc gọi các function với Completion Handle liên tiếp sẽ có nhược điểm như là :

  • Quá nhiều closure lồng nhau -> Code rất dài và khó đọc
  • Khó quản lý lỗi
  • Chỉ cần quên gọi Completion là block luôn các phần còn lại.

Ngoài ra còn một nhược điểm mà t và nhiều người nữa đã, đang và sẽ gặp trong việc sử dụng Completion Handle với các hàm bất đồng bộ là việc: quên từ khoá @escaping, vì nhiều khi bản thân không biết lúc nào function là đồng bộ hay bất đồng bộ . Việc thêm @escaping là vô cùng quan trọng đối với các function bất đồng bộ sử dụng Completion Handle , vì nó sẽ giữ lại closure lại trong bộ nhớ kể cả khi function kết thúc, cho đến khi closure được sử dụng. Quên từ khoá escaping, sẽ khiến closure không được gọi -> Bug bắt đầu từ đó đó 😆

Lời kết về Completion Handle

Qua ví dụ trên cùng với các phân tích, ta có thể thấy Completion Handle không thực sự hoàn hảo như chúng ta vẫn nghĩ. Nó có những nhược điểm cơ bản sau:

  • Quên gọi Completion -> dẫn dến block luồng
  • Quên gọi @escaping cho function bất đồng bộ -> Closure không được gọi
  • Sử dụng nhiều function với Completion Handle liên tiếp -> Code khó đọc, khó quản lý lỗi.

Nhưng đừng lo, Swift 5.5 đã cung cấp cho chúng ta giải pháp, đó là sử dụng 2 từ khoá asyncawait. Tiếp theo chúng ta sẽ tìm hiểu cách sử dụng async/await.

Cơ bản về async và await

Sẽ rất khó hiểu nếu chúng ta cố gắng định nghĩa thế nào là asyncawait, nên chúng ta có thể hiểu đơn giản: hai từ khoá asyncawait để khai báo và gọi các function bất động bộ.

  • async: dùng trong khai báo một function, đánh dấu cho chúng ta biết đó là function bất động bộ
  • await: dùng khi gọi để thực thi các function bất đồng bộ

Cú pháp

1.Đánh dấu 1 function bất đồng bộ với async
func someFunction() async -> Void {
// do something
}
2. Gọi function bất đồng bộ với await
  • Trường hợp gọi function với await ngay trong một đoạn code đồng bộ, thì cần phải bọc trong Task{...}
func synchronousFunction {
// do something
Task {
await someFunction()
}
}
  • Trường hợp gọi trong một function bất đồng bộ khác
func anotherFunction() async {
await somefunction()
}

Ví dụ

Chúng ta sẽ triển khai lại với bài toán đã có ở trên.

  • Việc đầu tiên cần làm là viết lại function fetchThumnailImage, vẫn nhận vào là một urlString nhưng không có cần phải sử dụng Completion Handle để pass dữ liệu nữa, mà sẽ sử dụng async và trả về luôn data là UIImage như sau:
func fetchThumnailImage(from urlString: String) async throws -> UIImage {
guard let url = URL(string: urlString) else {
throw CustomError.badUrl
}
// call request..
}

Vì không phải lúc nào cũng có thể may mắn trả về ngay một UIImage, nên sẽ sử dụng throws trong trường hợp không may xảy ra lỗi 😅

  • Tiếp theo, là xử lý download ảnh từ URL. Rõ ràng, đây là 1 xử lý bất đồng bộ, cần phải đánh dấu function bất đồng bộ với async và Swift đã giúp ta làm việc đó với với function: url_session_async Việc chúng ta cần làm bây giờ, là sử dụng await để gọi nó ra. Tuy nhiên vì function mà swift cung cấp có sử dụng throws, nên chúng ta phải sử dụng cú pháp try await như sau:
func fetchThumnailImage(from urlString: String) async throws -> UIImage {
guard let url = URL(string: urlString) else {
throw CustomError.badUrl
}
let urlRequest = URLRequest(url: url)
// call request..
let (data, _) = try await URLSession.shared.data(for: urlRequest)
let image = UIImage(data: data)
// handle thumbnail image..
}
  • Xử lý call request đã xong, bước tiếp theo sẽ là xử lý đưa ảnh origin -> ảnh thumbnail, và swift cũng đã hỗ trợ function byPreparingThumbnail(ofSize:) đã được dánh dấu với await và chúng ta chỉ việc sử dụng:
func fetchThumnailImage(from urlString: String) async throws -> UIImage {
guard let url = URL(string: urlString) else {
throw CustomError.badUrl
}
let urlRequest = URLRequest(url: url)
// call request..
let (data, _) = try await URLSession.shared.data(for: urlRequest)
let image = UIImage(data: data)
// handle thumbnail image
guard let thumbnail = await image?.byPreparingThumbnail(ofSize: CGSize(width: 30, height: 30)) else {
throw CustomError.badImage
}
return thumbnail
}
  • Đó là toàn bộ đoạn code xử lý cho bài toán đã đặt ra, để gọi function fetchThumnailImage đã được đánh dấu bất đồng bộ với async, trong 1 luồng đang chạy đồng bộ như các xử lý trong viewDidLoad, ta sẽ gọi như sau:
override func viewDidLoad() {
super.viewDidLoad()
let urlString = "https://d38b044pevnwc9.cloudfront.net/cutout-nuxt/enhancer/2.jpg"
Task {
do {
let image = try await fetchThumnailImage(from: urlString)
self.imageView.image = image
} catch {
self.lbError.text = error.localizedDescription
}
}
}

Kết luận

Rõ ràng, cùng một bài toán, xử lý với Async/await sẽ ngắn gọn và dễ nhìn hơn rất nhiều so với Completion Handle. Từ đó mà có thể dễ dàng quản lý lỗi, đọc code và không cần lo lắng bị block luồng hoạt động vì lỡ quên cái gì đó như @escaping hay completion

Compare_async_completion.jpg

=> Async/await khắc phục được các nhược điểm mà chúng ta đã đề cập ở trên của Completion Handle. Tuy nhiên, nó được sinh ra không chỉ làm những nhiệm vụ đơn giản như vậy. Chúng ta sẽ đi sâu tiếp về async/await ở blog tiếp theo.

Cảm ơn mọi người vì đã đọc đến đây. Hẹn gặp lại ở bài viết tiếp theo. 🥰 Thân ái và quyết thắng 🇻🇳🇻🇳🇻🇳🇻🇳🇻🇳


Tags

swiftiOS

Share

phuong.bui

phuong.bui

Developer

Expertise

Related Posts

Concurrency và Async/Await ~ Part 2
Solutions
Concurrency và Async/Await ~ Part 2
December 04, 2022
8 min
© 2023, All Rights Reserved.
Powered By

Quick Links

HomeOur Team

Social Media