Contents
Chào mừng bạn đến với Fx Studio. Bài viết này, chúng ta sẽ cùng nhau khám phá một chủ đề mới có trong Swift 5.5. Đó là async/await. Và đây cũng là gì bạn sẽ phải chuẩn bị để tiếp tục hành trình Concurrency với Swift 5.5.
Nếu bạn chưa biết gì về ngôn ngữ lập trình Swift thì có thể tham khảo bài viết sau:
Còn nếu mọi thứ đã ổn rồi, thì …
Bắt đầu thôi!
Cập nhật mới
Hiện tại, Xcode 13.1 trở đi thì việc sử dụng trực tiếp cú pháp async { ... }
sẽ được thay bằng Task { ... }
. Các phần còn lại vẫn không thay đổi. Do đó, bạn cũng không cần quá lo lắng và yên tâm bắt đầu khám phá hành trình New Concurrency trong Swift 5.5 nhóe!
Chuẩn bị
Sự kiện lớn nhất của Apple vào đầu tháng 6/2021 vừa qua là WWDC 21. Apple đã giới thiệu cho cộng đồng dev iOS về Swift 5.5, trong đó có sự cập nhật rất là lớn. Đó là về bất đồng bộ, với async
và await
. Do đó, bạn cần phải chuẩn bị như sau:
-
- Swift 5.5
- Xcode 13 (beta)
Thật là khó khi Xcode 13 khá là nặng, nên có thể Macbook của bạn sẽ chạy không nỗi. Nên bạn cũng phải cân nhắc khi cập nhật macOS và Xcode 13 nhoé.
Nói về async/await, đây là 2 keyword mới được thêm vào Swift 5.5. Nhưng về ý nghĩa của nó mang tới thì khá to lớn. Nó giúp cho function của bạn đa năng hơn. Ngoài ra, các cập nhất của Swift 5.5 còn sẽ sử dụng thêm async/await. Điều quan trong nhất là async/await đặt nền nóng cho sự phát triển tiếp theo của Concurrency.
Swift 5.5 được ra mắt với nhiều sự cập nhật liên quan tới Bất đồng bộ.
Còn với bài viết này, mình sẽ trình bày những điều cơ bản nhất giúp bạn có thể nắm bắt nhanh async/await trong vòng 10 phút (hoặc lâu hơn). Vì mọi thứ vẫn đang là bảng beta, nên nếu có cập nhật gì mới thì mình sẽ bổ sung tiếp vào bài viết này.
1. Vấn đề với Completion Handle
Completion Handle là thứ mà bạn và các đồng nghiệp iOS developer của bạn đã và đang dùng ngày qua ngày. Hầu như nó là một phần máu thịt đối với mỗi dev iOS rồi. Điểm hình như:
- Trả về một Callback
- Thay thế các Delegate & DataSource
- Trả Result về trong thương tác connect API
- Các xử lý tính toán logic khác
Tuy nhiên ….
1.1. Ví dụ
Chúng ta sẽ phân tích một ví dụ cơ bản cách truyền thống với Completion Handle. Ví dụ: ta có 2 hàm xử lý với closure để trả kết quả về.
func cong(a: Int, b: Int, completion: @escaping (Int) -> Void) -> Void { DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { completion(a + b) } } func nhan(a: Int, b: Int, completion: @escaping (Int) -> Void) -> Void { DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { completion(a * b) } }
Hai function trên có điểu đặc biệt là sau một thời gian là 3 giây thì trả kết quả về. Ta sử dụng chúng xem ổn không nhoé.
let A = 10 let B = 20 // Gọi đơn giản cong(a: A, b: B) { result in print("cộng OKE nè : \(result)") } // Gọi lồng nhau cong(a: A, b: B) { result in print("cộng OKE nè : \(result)") nhan(a: A, b: B) { result in print("nhân OKE nè : \(result)") cong(a: A, b: B) { result in print("cộng OKE 2 nè : \(result)") nhan(a: A, b: B) { result in print("nhân OKE 2 nè : \(result)") } } } }
Về cách dùng thì mình không nói tới. Tuy nhiên, bạn xem cách gọi các function lồng nhau. Đúng là một sự huỷ diệt.
(Primary of doom)
Bạn thấy việc lồng nhau như vậy ổn không? Và bạn dám chắc là bạn có quản lý việc gọi lẫn nhau đó tốt không?
1.2. Các vấn đề khác
Qua ví dụ trên, bạn sẽ thấy một số điều bất hợp lý trong Completion Handle với Closure mà điển hình như sau:
- Gọi completion có thể được gọi nhiều lần hoặc không gọi do quên
- Thêm từ khoá
@escaping
nếu function là bất đồng bộ. Khó tiếp cận và sẽ rối với những bạn mới vào nghề. Vì chỉ nhờ Xcode thêm vào, còn bản thân không biết lúc nào là đồng bộ hay bất đồng bộ. - Nếu như gọi nhiều function với Completion Handle liên tiếp, để nhằm mục đích là chờ hàm này xong rồi mới thực hiện hàm kia.
- Gây khó chịu trong cú pháp gọi.
- Nhiều closure lồng nhau.
- Khó quản lý lỗi.
- Quên gọi Completion thì sẽ bock các phần còn lại.
- Với Result type trong Swift 5.0 thì sẽ khó trong việc
Callback
lại Error.
Đúng là …
2. Async/Await
Qua những lý do trên, Swift 5.5 đã cung cấp thêm 2 từ khoá async & await trong việc khai báo và gọi function. Cho phép chúng ta chạy các đoạn code bất đồng bộ nếu chúng chạy bất đồng bộ.
Cách dùng khá là EZ:
- Dùng
async
trong khai báo function - Dùng
await
trong gọi thực thi function
2.1. Cú pháp
Khai báo function với async
func somefunction() async -> Void { // bla bla bla }
Cách thực thi thì sẽ chia ra làm 2 loại:
- Gọi hàm trong code đồng bộ để thực thi function với await. Cần có
async { }
để bọc lại.
async { await somefunction() }
- Gọi thực thi trong một function bất đồng bộ khác
func anotherFunction() async { await somefunction() }
2.2. Ví dụ
Để dễ hiểu hơn thì ta hãy viết lại 2 function trên với async/await như thế nào. Có thêm trường hợp function có trả giá trị về.
func cong(a: Int, b: Int) async -> Int { a + b } func nhan(a: Int, b: Int) async -> Int { a * b }
Khá là đơn giản phải không nào. Giờ chúng ta tiếp tục thực hiện việc gọi function đó chạy xem kết quả tra sao. Phần này bạn tự kiểm tra nha. Lưu ý, để khỏi vất vả config thì bạn hãy tạo 1 project và thực thi nó, đừng chạy với Playground.
Nhớ import thêm UIKit để chạy
async { }
nhé, nếu bạn là người chơi hệ Playground.
Chúng ta tiếp tục với việc sử dụng hàm bất đồng bộ để gọi hàm bất đồng bộ. Xem ví dụ nha.
func tinh() async { let A = 10 let B = 20 print("... #1") let Cong = await cong(a: A, b: B) print("... #2") let Nhan = await nhan(a: A, b: B) print("2 kết quả nè: \(Cong) & \(Nhan)") }
Bạn thấy chúng ta đã gọi các hàm cong
& nhan
. Với từ khoá await
thì chương tình sẽ dừng chờ tới khi nào function kia thực hiện xong và trả kết quả về. Sau đó, dòng lệnh tiếp heo mới được thực thi.
async { await tinh() }
Khi thực thi hàm tinh()
thì:
- Chương trình dừng chờ tại dòng code tính
Cong
, sau đó chạy dòng lệnh tiếp theo. - Chương trình tiếp tục với
Nhan
và cũng tính & chờ. - Khi cả 2 biến có đầy đủ giá trị thì lệnh print cuối cùng mới được thực thi.
2.3. Quy tắc
Ta cũng có một số quy tắc sau cho async/await
- Các hàm đồng bộ (Synchronous) sẽ không gọi trực tiếp được các hàm bất đồng bộ
- Các hàm bất đồng bộ có thể gọi được các hàm bất đồng bộ khác và các hàm đồng bộ bình thường khác
- Nếu các hàm đồng bộ & bất đồng mà giống nhau về cách gọi hàm. Thì Swift sẽ dựa vào ngữ cảnh để gọi.
3. Error
Với Completion Handle mà cụ thể ở đây là bạn sẽ sử dụng Closure. Chúng ta hầu như sẽ không throw error
. Thay vào đó, ta lại ném chúng thành đối số cho việc gọi closure để call back trở lại.
Điều này nó hơi phản khoa học một chút. Tuy nhiên, mọi người vẫn nhắm mắt mà làm.
Còn với async/await, bạn có thể giải toả nỗi lo này rồi.
- Khi đưa ra async/await thì bạn có thể dùng với try/catch
- Bạn có thể
throw error
trong các function bất đồng bộ hoặc khởi tạo bất đồng bộ - Thứ tự throw lỗi sẽ ngược lại với thứ tự gọi hàm
3.1. async throws
Chúng ta bắt đầu kết hợp tụi nó lại với nhau. Đầu tiên, bạn cần khai báo thêm một Error của riêng bạn nhoé.
enum MyError : Error { case soBeHon case soAm case bangKhong }
Ta sẽ viết mới 2 function, với có sự nén lỗi trở lại. Xem ví dụ code nhoé.
func tru(a: Int, b: Int) async throws -> Int { if a < b { throw MyError.soBeHon } else { return a - b } } func chia(a: Int, b: Int) async throws -> Float { if b == 0 { throw MyError.bangKhong } else { return Float(a) / Float(b) } }
Dựa vào điều kiện của các tham số mà chúng ta throw
lỗi lại. Cái này cũng không quá khó và chắc bạn đã làm khá nhiều rồi. Chủ yếu bạn cần nhìn vào dòng khai báo function. Chúng ta cần có async throws
là ổn hết.
3.2. try await
Sau khi khai báo thành công, thì chúng ta sẽ thực thi tụi nó. Ngược lại với async throws là try await. Và nó cũng không khó, bạn cần kết hợp 2 công việc cơ bản lại là ổn:
- Cú pháp
do catch
vàtry
để cố gắng thực hiện việc gì đó mà sẽ có lỗi await
dùng để chờ hàm bất đồng bộ hoàn thành công việc của nó
Ví dụ cho việc gọi 2 function vừa tạo ở trên như sau:
func tinh2() async { let A = 30 let B = 20 do { print("... #1") let Tru = try await tru(a: A, b: B) print("... #2") let Chia = try await chia(a: A, b: B) print("2 kết quả nè: \(Tru) & \(Chia)") } catch { print("Lỗi nhoé!") } }
Cách gọi hàm trong code đồng bộ.
async { await tinh2() }
Bạn tự thay đổi giá trị ban đầu để xem chúng nó thay đổi gì nha.
3.3. Ý nghĩa
- Khi thêm việc
throw error
giúp cho bạn cải thiện nhiều hơn về mặt diễn giải logic của code. - Giúp làm cho kiểu Result bớt đi áp lực, mà nó phải gánh khi bạn có nhiều loại Error và đôi lúc bạn phải miễn cưỡng chấp nhận nó.
- Sử dụng chúng sẽ không làm cho code của bạn trở lên magic hơn. Mà chỉ giúp bạn giảm tải đi rất nhiều code lồng nhau khi gọi liên tiếp nhiều hàm bất động bộ mà thôi.
- Theo khuyến khích của Apple, bạn không nên gọi hàm bất động bộ trong các hàm đồng bộ. Nếu bất khả khán thì hãy dùng
async { ... }
nhoé.
4. Checked Continuation
Phần này, mình sẽ trình bày về cách sử dụng các đoạn mã đồng bộ với các tác vụ bất đồng bộ, bằng cách sử dụng async
.
Nghe hơi khó hiểu phải không nào.
Nôm na phần này, bạn sẽ sử dụng khá là nhiều khi việc tương tác API. Kết hợp với async/await để cho nó hợp thời thượng nữa.
Chúng ta sẽ bắt đầu tìm hiểu qua các ví dụ sau:
4.1. DispatchQueue truyền thống kết hợp với Closure
Nói về DispatchQueue thì không có gì để nói hết. Bạn dùng nó như cơm bữa hằng ngày. Bây giờ, ta ví dụ lại để hồi tưởng tí. Bắt đầu, ta cần tạo 1 Error.
enum APIError: Error { case anError }
Bạn viết thêm 1 function. Ở đây, bạn giả định đây làm hàm gọi API nha. Xem ví dụ nhoé.
func fetchLatestNews(completion: @escaping ([String]) -> Void) { DispatchQueue.main.async { completion(["Swift 5.5 release", "Apple acquires Apollo"]) } }
Trong đó,
completion
trả về Result@escaping
để không bị mất đi khi thực hiện bất đồng bộ- Kiểu trả về là Void với hệ người chơi vô trách nhiệm
Tiếp tục, bạn thực thi nó.
fetchLatestNews { strings in for str in strings { print(str) } }
Cũng không có gì phức tạp hết. Ahihi! mục đích giúp bạn hồi tưởng lại kiến thức thôi.
4.2. async/await
Với async/await khi bạn sử dụng với function trên thì vẫn ổn. Nhưng nếu bạn sử dụng một thư viện bên ngoài (nhất là cái liên quan tới connect API), bạn khó lòng thay đổi lại nội của nó.
Mục đích là cho nó hợp thời mà thôi nhoé.
Tiếp theo, bạn cũng biết được rằng async func
thì sẽ chỉ được triệu hồi bởi các function bất đồng bộ mà thôi. Và nếu, bạn phải sử dụng code đồng bộ bình thường. Thì nó là một vấn đế khá đau đầu nữa nhoé.
Tất nhiên, ta sẽ có một giải pháp mới cung cấp cho việc kết hợp async code & sync code với nhau:
withCheckedContinuation()
giúp bạn tiếp tục sử dụng để triệu hồi bất đồng bộ trong đồng bộresume(returning:)
giúp bạn trả về giá trị mong muốn
4.3. Await Checked Continuation
Ta sẽ viết lại function trên với một cách hoàn toàn mới. Bạn sẽ cảm nhận sự khác biệt sớm thôi, ahihi.
func fetchLatestNews() async -> [String] { await withCheckedContinuation({ c in c.resume(returning: ["Fx", "Studio"]) }) }
Chúng ta từ bây giờ sẽ nói lời chia tay với DispatchQueue là được rồi.
Đó là điểm khác biệt mà async/await mang tới cho bạn. Mọi việc sẽ đơn giản hơn cho bạn. Tiếp theo, bạn xem cách thực thi function đó như thế nào. (À cũng giống như trên thôi)
async { let items = await fetchLatestNews() for item in items { print(item) } }
Tiếp tục demo lại với việc triệu hồi function trong vỏ async. Có điểm khác biệt đó là:
- Bạn không còn phải lo lắng khi nào có
@escaping
nữa. Vì đã là bất đồng bộ & với khai báo async thì nó sẽ hoạt động theo bất đồng bộ. - Kết quả nhận được sẽ chờ await sau khi hàm xử lý xong và trả về.
- Các đoạn code ở dưới vẫn chạy tốt và không có crash. Bởi vì nó chờ hàm bất đồng bộ thực thi xong trước rồi.
4.4. With Error
Ta sẽ tiếp tục thực thi code bất đồng bộ trong đồng bộ, mà chúng có thể sinh ra lỗi. Một điều cũng khá hiển nhiên, khi bạn thực thi một việc gì đó và có nguy cơ sinh ra lỗi.
Bạn chỉ cần thêm async throws
cho function bất đồng bộ để có thể nén lỗi về lại. Tham khảo ví dụ dưới nha.
func fetchLatestNews2() async throws -> [String] { try await withCheckedThrowingContinuation({ c in /// chỗ này bạn gọi API nè fetchLatestNews { items in if Bool.random() { /// Giả sử đúng là API trả về kết quả nhoé c.resume(returning: items) } else { /// Đây là chỗ bạn trả về lỗi nhoé c.resume(throwing: APIError.anError) } } }) }
Mình có chú thích ở trong code luôn rồi đó, bạn đọc và tự ngẫm nha. Còn cách dùng như sau:
async { do { let items = try await fetchLatestNews2() for item in items { print(item) } } catch { print("Lỗi nè") } }
Mọi thứ cũng không có gì phức tạp, thêm do catch
để bắt các error mà thôi. Cuối cùng, bạn hãy thử nghiệm với resume 2 lần trong withCheckedContinuation
. Xem điều kì diệu là gì?
5. Unsafe Continuation
Nếu như Xcode báo lỗi khi bạn cố resume 2 lần trở lên trong withCheckedContinuation
. Lý do, là Xcode sẽ đảm bảo code của bạn an toàn hơn với runtime
. Và cũng để đảm bảo tính bảo toàn của các Thread hay các xử lý đồng thời.
Nếu bạn là người chơi hệ mạo hiểm và muốn bỏ qua hết các cảnh báo, thì hãy tiếp tục thực thi code bất đồng bộ bỏ qua việc check an toàn.
Với cách dùng này, bạn sẽ thoải mái hơn nhiều. Mà việc check với safe không làm được.
- resume được nhiều lần. Nhưng sẽ hên xui.
- Tăng được tốc độ runtime
Ta sẽ viết lại một function mới với cách dùng mới nha. Xem code tham khảo.
func fetchLatestNews3() async -> [String] { await withUnsafeContinuation({ uc in uc.resume(returning: ["Fx2", "Studio2"]) uc.resume(returning: ["Fx3", "Studio3"]) }) }
Có một chỗ khác ở trên là withUnsafeContinuation
mà thôi. Sau đó, bạn gọi resume
2 lần xem thử.
async { let items = await fetchLatestNews3() for item in items { print(item) } }
Thực thi nhoé, chương trình của bạn vẫn bình an vô sự.
Lưu ý:
- Cần có
resume
, nếu không sẽ bị leak bộ nhớ. Còn nếu 2 lần thì có sự cố nhoé - Swift sẽ báo cho bạn biết
resume
nếu bị gọi 2 lần. - Nếu bạn mặc kệ dòng đời và méo quan tâm runtime như thế nào, thì hãy dùng
withUnsafeContinuation()
- Tại playground thì nó dễ chiếm sóng cái kia, nên bạn hãy cẩn thận khi dùng chúng nhoé
Tạm kết
- Giới thiệu về async/await trong Swift 5.5. Các cách khai báo và cách dùng async/await cho function.
- Các vấn đề gặp phải với Completion Handler.
- Cách giải quyết các vấn đề của Completion với async/await.
- Sử dụng function với Error trong async/await.
- Thực thi tương tác giữa code bất đồng bộ với các tác vụ đồng bộ trong 2 trường hợp kiểm tra an toàn và không an toàn.
Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về async/await trong Swift 5.5 . Như đã trình bày ở trên, nếu có gì thay đổi hay cập nhật thêm, mình sẽ tiếp tục update cho bài viết. Với Swift 5.5 còn rất nhiều những thứ hay nữa, nên bạn hãy tiếp tục theo dõi và chờ đón các bài viết sau nha.
Và nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
- Bài viết tiếp theo tại đây.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)