Hi friends, ở phần trước t đã giới thiệu cơ bản về cách sử dụng async
và await
trong swift. Ngày hôm nay, chúng ta sẽ tiếp tục đi tiếp series này và cùng nhau tìm hiểu những điều hay ho hơn về Concurrency mà chắc chưa nhiều người đã biết. 😆
Dành cho những ai chưa đọc part1 của mình, thì trước khi bắt đầu, hay đọc qua nó để biết thêm nhé.
Part 1- Cơ bản về async và await
Let’s go !!!
Ở phần trước t có nói, async
được sử dụng để đánh dấu một function là bất đồng bộ. Tuy nhiên, chúng ta hoàn toàn có thể sử dụng async
để làm việc đó cho một thuộc tính. Bất kì thuộc tính nào được đánh dấu async
đều có thể tự do call các APIs bất đồng bộ và khi có đoạn code truy cập đến một thuộc tính như vậy, hệ thống sẽ tạm ngưng cho đến khi hoàn thành xong tất cả các hoạt động không đồng bộ đó, mới chạy đến đoạn code tiếp theo.
Ở phần trước, khi muốn convert một ảnh sang một ảnh thumbnail, chúng ta có một đoạn code như sau:
func fetchThumnailImage(from urlString: String) async throws -> UIImage {// ....let image = UIImage(data: data)guard let thumbnail = await image?.byPreparingThumbnail(ofSize: CGSize(width: 30, height: 30)) else {throw CustomError.badImage}return thumbnail}
Tuy nhiên, chúng ta có thể rút ngắn đoạn code trên hơn nữa, bằng cách khai báo một thuộc tính bất đồng bộ, là extension UIImage
với cấu trúc như sau:
extension UIImage {var thumbnail: UIImage? {get async {let size = CGSize(width: 30, height: 30)return await self.byPreparingThumbnail(ofSize: size)}}}
Thuộc tính thumbnail
đã được đánh dấu là bất đồng bộ bằng cách đánh dấu async
ở ngay sau get
.
Sau khi khai báo thuộc tính thumbnail
, đoạn code ban đầu có thể được rút gọn như thế này:
func fetchThumnailImage(from urlString: String) async throws -> UIImage {// ....let image = UIImage(data: data)guard let thumbnail = await image?.thumbnail else {throw CustomError.badImage}return thumbnail}
Ultr, số dòng code thì có vẻ nhiều hơn xíu đấy, nhưng có phải nhìn đoạn code đã gọn gàng và sạch sẽ hơn rất nhiều đúng không? Và sau đó thì việc convert một ảnh gốc sang ảnh thumbnail có thể sử dụng dễ dàng ở bất cứ đâu trong code của bạn. Thật tiện lợi 😜
Tuy nhiên, khi sử dụng thuộc tính bất đồng bộ, chúng ta cần lưu ý hai điều sau đây:
getter
rõ ràng, đây là điều bắt buộc, và chúng ta sẽ đánh dấu thuộc tính bất đồng bộ bằng cách đặt async
ngay sau get
như ở ví dụ trênsetter
. Chỉ có thuộc tính only-read mới có thể trở thành một thuộc tính bất đồng bộ.AsyncSequence
là một biến thể của Sequence
. Cũng giống như Sequence
, nó cung cấp một danh sách các giá trị mà chúng ta có thể thao tác được, tuy nhiên nó được thêm tính không đồng bộ. Chính vì không đồng bộ nên một AsyncSequence
có thể có tất cả, một ít, hoặc không giá trị nào khi nó được gọi lần đầu tiên. Thay vào đó, chúng ta sử dụng await
để đợi và nhận tất cả các giá trị khi nó đã tồn tại.
Tuy nhiên, có một điều quan trọng cần phải hiểu rằng AsyncSequence
mà swift cung cấp cho chúng ta, chỉ là một protocol. Nó cho chúng ta biết cách truy cập các giá trị, nhưng nó không chứa các giá trị.
Dev phải triên khai giao thức AsyncSequence
nếu muốn sử dụng nó. Để triển khai, chúng ta phải cung cấp cho nó một AsyncIterator
- được dùng để tạo ra các phần tử của chuỗi không đồng bộ này.
Chẹp, khó hiểu rồi phải không? Thôi thì mình sẽ giải thích thông qua ví dụ về một AsyncSequence
dưới đây để hiểu rõ hơn nhé!
struct DownloadImageList: AsyncSequence{// 1.typealias Element = UIImage// 2.let listUrl: [URL]// 3.struct AsyncIterator: AsyncIteratorProtocol {// 4.let listUrl: [URL]// 5.var current = 0// 6.mutating func next() async -> UIImage? {guard current < listUrl.count else { return nil }let urlRequest = URLRequest(url: url)guard let (data, _) = try? await URLSession.shared.data(for: urlRequest) else {return UIImage.imgDefault}let image = UIImage(data: data)current += 1return image}}// 7.func makeAsyncIterator() -> AsyncIterator {return AsyncIterator(listUrl: listUrl)}}
Đoạn code trên thể hiện cấu trúc cơ bản một AsyncSequence
, chúng ta hãy đi phân tích từng dòng code :
AsyncSequence
-> RequiredAsyncIterator
implement protocol AsyncIteratorProtocol
: được dùng đại diện cho một vòng lặp không đồng bộ, tạo ra các phần tử cho chuỗi không đồng bộ này -> RequiredAsyncIterator
AsyncIteratorProtocol
định nghĩa method next()
. Method này là bắt buộc phải có để có thể tạo ra các phần tử hiện tại, nó được đánh dấu async
để yêu cầu người gọi phải đợi cho đến khi nó tạo ra dc một phần tử có giá trị. Một khi method return về nil
, nó thông báo cho AsyncSequence
biết rằng đã hết phần tử cần tạo và kết thúc vòng lặp để ngừng tạo phần tử của chuỗi. -> RequiredAsyncIterator
đã được khai báo ở bước 6 -> tạo ra một vòng lặp không đồng bộ với mục đích tạo ra các phần tử cho chuỗi không đồng bộ -> RequiredUltr, giải thích một hồi vẫn khó hiểu đúng k? vậy thì hãy xem lại đoạn code trên và ngẫm lại lần nữa, lần nữa, và b sẽ hiểu thôi. T đã phải làm vậy rất lâu đấy 😆
Và từ đây, việc triển khai và gọi AsyncSequence
rất đơn giản, nó như một Sequence
bình thường thôi, chỉ khác là sẽ thêm await
ngay sau for
for await item in DownloadImageList(listUrl: url) {print("image", item)}print("Finish")
Output sẽ là lần lượt các ảnh theo URL, sau đó ms log ra Finish
. Keyword await
đã buộc người dùng phải đợi để nhận hết các giá trị không đồng bộ - thời điểm mà method next()
trả về nil
- sau đó mới chạy đến dòng code tiếp theo.
Tương tự như Sequence
, nhiều operators cũng có thể sử dụng tương tự đối với AsynceSequence
như map(:_)
, filter(:_)
, …
Như chúng ta đã biết từ đầu dến giờ, việc gọi đến một function không đồng bộ với await
chỉ chạy một đoạn code tại một thời điểm, và khi đoạn code không đồng bộ được chạy, người gọi sẽ phải đợi cho đến khi nó hoàn tất rồi mới chạy đến dòng code tiếp theo. Ví dụ đoạn code dưới đây :
let img1 = await fetchThumnailImage(from: urlString1)let img2 = await fetchThumnailImage(from: urlString2)let img3 = await fetchThumnailImage(from: urlString3)let imgs = [img1, img2, imge3]show(imgs)
Mặc dù fetchThumnailImage
là một function bất đồng bộ, và các công việc khác vẫn diễn ra ngay cả khi nó đang chạy. Tuy nhiên, chỉ một fetchThumbnailImage(from:)
được gọi tại một thời điểm. Mỗi ảnh phải được hoàn tất trước lần download tiếp theo.
Tuy nhiên,vẫn có cách để gọi các function bất đồng bộ song song và độc lập tại cùng một thời điểm mà không cần phải chờ hay đợi nhau. Để làm được điều này, chỉ cần thêm async
vào trước let
khi chúng ta define 1 biến, và gọi await
mỗi khi sử dụng biến đó. Ví dụ đoạn code trên có thể thay đổi như sau
async let img1 = fetchThumnailImage(from: urlString1)async let img2 = fetchThumnailImage(from: urlString2)async let img3 = fetchThumnailImage(from: urlString3)let imgs = await [img1, img2, imge3]show(imgs)
Ở ví dụ trên, cả 3 lệnh gọi fetchThumbnailImage(from:)
đều được bắt đầu gọi mà không cần đợi lệnh trc hoàn thành. Nếu đủ tài nguyên hệ thống, chúng có thể chạy cùng một lúc. Không có keyword await
vì vậy mà không cần đoạn code nào cần phải tạm dừng, tuy nhiên, khi đến bước hiển thị hình ảnh thì cần kết quả từ các fetchThumbnailImage(from:)
, do đó mà chúng ta phải thêm await
để tạm dừng thực thi cho đến khi cả 3 lệnh hoàn tất thì ms đến xử lý show(imgs)
Từ ví dụ trên, ta có thể thấy rút ra được một vài điểm khác nhau giữa các gọi await
và async-let
như sau:
await
khi các dòng code sau phụ thuộc, liên quan đến kết quả của function. Điều này khiến code được hiện một cách tuần tự.async-let
khi chúng ta không cần quan tâm đến kết quả của function ở các đoạn code sau. Điều này tạo ra các công việc được hiện song song.await
và async-let
điều cho phép đoan code khác thựuc thi khi mà chúng đang bị ngừng - vì điều là function bất đồng bộ.await
để có thể buộc người gọi phải phải dừng để đợi cho đến khi đoạn code bất đồng bộ hoàn tất rồi mới đến dòng code tiếp theo.Ở phần trước, trong phần giới thiệu về cách gọi function bất đồng bộ với await
trong đoan code đồng bộ, thì cần phải bọc nó trong Task {...}
, nhưng Task
sử dụng cụ thể như thế nào thì chúng ta sẽ cùng tìm hiểu ngay bây giờ.
Task
là một kiểu dữ liệu mới dc Swift 5.5 thêm mới vào Structured Concurrency
, nó tạo một đối tượng để thực thi và chạy các tác vụ bất đồng bộ như các code đồng bộ khác. Hiểu đơn giản, muốn gọi các fucntion bất đồng bộ trên một luồng đồng bộ thì ta có thể sử dụng Task
.
Task(priority: <TaskPriority?>, operation: <() async -> _>)
priority
: tham số thể hiện cấp độ ưu tiên của tác vụoperation
: công việc cần thực thi. Mặc định, thì công việc sẽ là function với async, và sẽ có giá trị trả về hoặc không.Ví dụ:
override func viewDidLoad() {super.viewDidLoad()Task {let img1 = await fetchThumnailImage(from: urlString1)print("Finish")}print("Finish2)}
Ví dụ trên t đã gọi function bất đồng bộ fetchThumbnailImage
ngay trên main thread . Output tất nhiên sẽ log ra Finish2
rồi ms log ra Finish
vì trong Task
là function bất đồng bộ mà 😆
Một điều khá thú vị ở Task
là chúng ta có thể chủ động dừng các hoạt động bất động bộ trong Task
bất cứ lúc nào bằng cách sử dụng function cancel()
. Trong mỗi task
có thể kiểm tra xem nó đã bị huỷ hay chưa bằng cách gọi Task.checkCancellation()
, function sẽ trả về CancellationError
nếu task đã bị huỷ và ngừng thực thi tất cả các đoạn code sau khi Task.checkCancellation()
được gọi.
Tuỳ thuộc vào việc xử lý bất đồng bộ, mà việc huỷ ngang Task
giữa chừng có thể khiến Task
:
CacnellationError
nil
hoặc 1 tập rỗngVí dụ:
override func viewDidLoad() {super.viewDidLoad()let task1 = Task {await printNunber()try Task.checkCancellation()await printNunber2()}task1.cancel()}func printNunber() async {for i in 0..<10{print("😜", i)}}func printNunber2() async {for i in 0..<10{print("🇻🇳", i)}}
Ở đoạn code trên, task1
đã bị huỷ. Sau khi chạy xong function bất đồng bộ printNumber()
và đến lệnh Task.checkCancellation()
, nó nhận ra mình đã bị huỷ và nó sẽ ngừng tất cả hoạt động còn lại của Task. Output sẽ như sau.
Nếu comment đoạn code task1.cacnel
, output sẽ là kết quả của tất cả xử lý không đồng bộ trong task.
Qua các ví dụ trên, chắc mọi người đều đã hiểu về Task
, nhưng thực tế, ta không chỉ làm việc với 1 Task
. Việc sử dụng các Task riêng lẻ cho các công việc phức tạp, sẽ không mang lại hiểu quả cao. Những lúc như này, chúng ta có thể sử dụng tới Task Group
.
Task Group
là một tập hợp các task để thực hiện cùng nhau nhằm tạo ra 1 giá khi hoàn thành.
Chúng ta không thể tạo Task Group trực tiếp, mà phải sử dụng:
withTaskGroup
withThrowingTaskGroup
Hai cách trên là tương tự nhau, khác nhau ở withThrowingTaskGroup
có throw
để có thể ném về lỗi.
Các task con sẽ được thêm vào Task Group thông qua method addTask()
.
withTaskGroup(of childTaskResultType: ChildTaskResult.Type,body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult) async -> GroupResult)
Trong đó:
childTaskResultType
: Kiểu dữ liệu của task conbody
: c ông việc cần thực thi trong Task Group và kiểu dữ liệu trả về của nó.Ví dụ:
func downloadAllImage() async {do {let downloadImge = try await withThrowingTaskGroup(of: UIImage.self) { group ingroup.addTask {print("Download image 1")let image = try await self.fetchThumnailImage(from: self.urlString1)print("Download finish 1")return image}group.addTask {print("Download image 2")let image = try await self.fetchThumnailImage(from: self.urlString2)print("Download finish 2")return image}group.addTask {print("Download image 3")let image = try await self.fetchThumnailImage(from: self.urlString3)print("Download finish 3")return image}for try await value in group {print("Download Value: ", value)}print("Download finish all")}} catch {print("Error", error)}}override func viewDidLoad() {super.viewDidLoad()Task {await downloadAllImage()}}
Output cho đoạn code trên như sau:
Giải thích một chút cho chiếc output trên.
for try wait
mới thực sự xong và chạy đến lệnh print
tiếp theo.Bài viết trên, t đã đưa ra một vài kiến thức mới mẻ và có thể gặp nhiều khi sử dụng Concurrency
trong swift mới nói chung, và cách async/await
nói riêng như:
Tuy nhiên, Concurrency
của Swift 5.5 không chỉ có như vậy. M.n có thể tham khảo thêm ở đây:
https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html#
Ở bài viết tiếp theo, t sẽ tiếp tục đi tiếp về series Concurrency này. Hẹn gặp lại m.n tại đó.
Thân ái và quyết thắng 🇻🇳🇻🇳🇻🇳🇻🇳🇻🇳