HomeOur Team
Concurrency và Async/Await ~ Part 2

Concurrency và Async/Await ~ Part 2

By phuong.bui
Published in Solutions
December 04, 2022
8 min read

Hi friends, ở phần trước t đã giới thiệu cơ bản về cách sử dụng asyncawait 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 !!!

1. Async Properties

Ở 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.

Ví dụ

Ở 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:

  • Thứ nhất, thuộc tính bất đồng bộ cần có một 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ên
  • Thứ hai, thuộc tính không có setter. 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ộ.

2. AsyncSequence

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 += 1
return 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 :

  1. Định nghĩa kiểu dữ liệu của các phần tử được tạo bởi async sequence - chuỗi không đồng bộ này. Dòng code này là bắt buộc phải có khi muốn triển khai giao thức AsyncSequence -> Required
  2. Danh sách đầu vào của chuỗi không đồng bộ.
  3. Khai báo struct AsyncIterator 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 -> Required
  4. Danh sách đầu vào của AsyncIterator
  5. Biến đếm- cho biết phần tử số phần tử đã được sinh ra.
  6. 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. -> Required
  7. Trả về AsyncIteratorđã đượ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ộ -> Required

Ultr, 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(:_) , …

3. Gọi các hàm không đồng bộ song song

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 awaitasync-let như sau:

  • Gọi functions bất đồng bộ với 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ự.
  • Gọi các hàm bất đồng bộ với 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.
  • Cả awaitasync-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ộ.
  • Trong cả 2 trường hợp, điều sử dụng 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.

4. Task và Task Group

Ở 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ờ.

4.1. Task

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.

Cú pháp

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:

  • Ném về 1 lỗi như là CacnellationError
  • Trả về nil hoặc 1 tập rỗng
  • Trả về một phần của công việc đã hoàn thành

Ví 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. Consolelog1.png

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. log2.png

4.2. Task Group.

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.

Cú pháp.

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 ở withThrowingTaskGroupthrow để 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 con
  • body: 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 in
group.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:

log_taskgroup.png

Giải thích một chút cho chiếc output trên.

  • Ngay khi task dc add vào Task Group , task đã được chạy và log ra 3 dòng đầu tiên.
  • Vòng for để đợi và lấy giá trị của các Task sau khi nó hoàn thành xong. Ở đây, t đã cố tình setup cho ảnh 2 nặng hơn 1 chút. Vì vậy, thứ tự donwload xong sẽ hiển thị như trên ảnh => Chứng tỏ rằng các task chạy song song và không phụ thuộc, đợi chờ nhau.
  • Sau khi tất cả các task đã hoàn thành, vòng lặp for try wait mới thực sự xong và chạy đến lệnh print tiếp theo.

Tổng kết

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ư:

  • Async Properties
  • Async Sequences
  • Gọi các hàm bất đồng bộ - Async Functions song song
  • Task & Task Group

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 🇻🇳🇻🇳🇻🇳🇻🇳🇻🇳


Tags

swiftiOSConcurrency

Share

phuong.bui

phuong.bui

Developer

Expertise

Related Posts

Checked Continuation and Unsafe Continuation ~ Part 3
Solutions
Checked Continuation and Unsafe Continuation ~ Part 3
November 30, 2022
4 min
© 2023, All Rights Reserved.
Powered By

Quick Links

HomeOur Team

Social Media