Sử dụng async/await trong Swift.
Bài đăng này đã không được cập nhật trong 3 năm
- Cơ chế mới xử lý mới
async/await
đã được giới thiệu ởSwift 5.5
tại WWDC 2021 tho thấy sự tập trung và ưu tiên trong việc phát triển cơ chếconcurency
củaApple
. Mặc dù sẽ mất nhiều thời gian nữa đêasync/await
có thể trở nên phổ biến và dùng nhiều trong công việc phát triển app nhưng rõ ràng ta đã thấy sự thu hẹp đáng kể về khoảng cách cũng như thân thiện hơn với cácdeveloper
trong việc xử lýconcurency
.
1/ Thế nào là async/await
?
-
Chúng ta sẽ mất khá nhiều thời gian nếu đi chi tiết trong việc định nghĩa
async/await
nhưng để ngắn gọn thì chúng ta có thể hiểu là nó cho phép chúng ta đánh dấufunction
bất đồng bộ trongcode
với từ khóaasync
và yêu cầ chúng ta từ khóaawait
để gọi cácfunction
đó. Hệ thống sẽ tự động tổ chức cho chúng ta công việc như đợi chờ thực hiện công việc gì đó khi cácfunction
này hoàn thành mà không cần nhét vào trongblock
để xử lý như trước. -
Lấy ví dụ như ở
struct
DocumentLoader
cófunc
được đánh dấuasync
,func
này sử dụngAPI
URLSession
để tiến hành xử lý công việccall network
bất đồng bộ với từ khóaawait
đểdownload
data
từURL
được cho:
struct DocumentLoader {
var urlSession = URLSession.shared
var decoder = JSONDecoder()
func loadDocument(withID id: Document.ID) async throws -> Document {
let url = urlForForLoadingDocument(withID: id)
let (data, _) = try await urlSession.data(from: url)
return try decoder.decode(Document.self, from: data)
}
...
}
- Với từ khóa
async
đặt trong cácfunction
thì khi gọi cácfuntion
bất đồng bộ khác thì ta cần thêm từ khóaawait
để các tiến trình xử lý trongfunction
đó được xếp thứ tự hoàn thành theo thứ cácawait
được triển khai mà không cần sợ có cácblock
được xử lý ở thời điểm mà chúng ta không xác định rõ.
2/ Gọi async function
từ các synchorous context
:
- Một câu hỏi được đặt ra là làm sao để chúng ta có thể gọi
function
được đánh dấuasync
từ cáccontext
mà bản thân nó hoạt không hoạt động bất đồng bộ. Nếu chúng ta muốn sử dụngDocumentLoader
bên trên vớiUIKit
thì chúng ta cần đưafunction
loadDocument
vào trong mộtTask
như sau để tiến hành xử lý bất đồng bộ:
class DocumentViewController: UIViewController {
private let documentID: Document.ID
private let loader: DocumentLoader
...
private func loadDocument() {
Task {
do {
let document = try await loader.loadDocument(withID: documentID)
display(document)
} catch {
display(error)
}
}
}
private func display(_ document: Document) {
...
}
private func display(_ error: Error) {
...
}
}
- Chúng ta đã sử dụng cơ chế mặc định
do / try / catch
trong cơ chếerror handling
để tiến hành xử lý các công việc bất đồng bộ nhưng chúng ta không cần quan tâm đến nhưng thứ nhưweak self
để tránh việcretain cycle
. Thậm chí chúng ta không cần phải tự xử lý tiến hành cập nhậtUI
ở trênmain queue
.
3/ Cài tiến API
bất đồng bộ sẵn có với async/await
:
- Chúng ta sẽ cùng tìm hiểu một cách thức khác để cải tiến cơ chế xử lý hoạt động bất đồng bộ bằng cách thêm vào
async/await
pattern. Chúng ta sẽ cùng triển khai mộtCommentLoader
để load cáccomment
với việc sử dụngcomplietion handler
:
struct CommentLoader {
...
func loadCommentsForDocument(
withID id: Document.ID,
then handler: @escaping (Result<[Comment], Error>) -> Void
) {
...
}
}
- Nhìn như có vẻ chúng ta sẽ cần thêm
async/await
nhưng trong trường hợp này thì không nhé. Chúng ta sẽ tiến hành xử lý trongextention
củaCommentLoader
vớifunction
loadCommentsForDocument
sử dụngfunction
mớiwithCheckedThrowingContinuation
để có thểwrap
và gọifunction
sẵn có với từ khóaasync
như sau:
extension CommentLoader {
func loadCommentsForDocument(
withID id: Document.ID
) async throws -> [Comment] {
try await withCheckedThrowingContinuation { continuation in
loadCommentsForDocument(withID: id) { result in
switch result {
case .success(let comments):
continuation.resume(returning: comments)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
}
- Với cách triển khai trên giờ chúng ta có thể dễ dàng gọi
method
loadCommentsForDocument
sử dụng từ khóaawait
như khi gọi cácAPI
bất đồng bộ khác. Và sau đây là cách chúng taupdate
DocumentLoader
một cách tự động với cáccomment
đượcfetch
về mỗi khidocument
được load:
struct DocumentLoader {
var commentLoader: CommentLoader
var urlSession = URLSession.shared
var decoder = JSONDecoder()
func loadDocument(withID id: Document.ID) async throws -> Document {
let url = urlForForLoadingDocument(withID: id)
let (data, _) = try await urlSession.data(from: url)
var document = try decoder.decode(Document.self, from: data)
document.comments = try await commentLoader.loadCommentsForDocument(
withID: id
)
return document
}
}
4/ Sử dụng single output của Publisher trong Combine:
- Chúng ta cùng xem xét khả năng xử lý
async/await
mạnh mẽ củaCombine
. Với cơ chế gửi đivalue
trongstream
thì tất cả công việc chúng ta là đợi chờ mộtresult
được trả về từ luồng xử lý củaCombine
. Tiếp tục sử dụngtechnique
chúng ta đã triển khai trước đó với việv mở rộngprotocol
Publisher vớimethod
singleResult
sẽ trả về chúng ta giá trị đầu tiên và duy nhất đượcemit
bởi Publisher. Chúng ta vẫn sẽ sử dụngclosure
để xử lý cơ chếretain
giải phónginstance
AnyCancellable
khi mà các công việc xử lý dữ liệu đã xong.
extension Publishers {
struct MissingOutputError: Error {}
}
extension Publisher {
func singleResult() async throws -> Output {
var cancellable: AnyCancellable?
var didReceiveValue = false
return try await withCheckedThrowingContinuation { continuation in
cancellable = sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
continuation.resume(throwing: error)
case .finished:
if !didReceiveValue {
continuation.resume(
throwing: Publishers.MissingOutputError()
)
}
}
},
receiveValue: { value in
guard !didReceiveValue else { return }
didReceiveValue = true
cancellable?.cancel()
continuation.resume(returning: value)
}
)
}
}
}
- Đến đây thì chúng ta sẽ dễ nhận ra sự tiện lợi và hiệu quả của việc xử dụng
API
Combine
để xử lý cơ chếasync/await
như thế nào so với việc sử dụngclosure
như cách làm phổ thông:
struct CommentLoader {
...
func loadCommentsForDocument(
withID id: Document.ID
) -> AnyPublisher<[Comment], Error> {
...
}
}
...
let comments = try await loader
.loadCommentsForDocument(withID: documentID)
.singleResult()
All rights reserved