Coroutine Kotlin cho phép bạn viết mã không đồng bộ rõ ràng, đơn giản mà ứng dụng của bạn thích ứng trong khi quản lý các tác vụ dài hạn như lệnh gọi mạng hoặc hoạt động của ổ đĩa.
Chủ đề này cung cấp thông tin chi tiết về coroutine trên Android. Nếu bạn chưa quen với coroutine, hãy nhớ đọc bài viết về coroutine của Kotlin trên Android trước khi đọc chủ đề này.
Quản lý các tác vụ chạy trong thời gian dài
Coroutine được xây dựng dựa trên các hàm thông thường bằng cách thêm hai thao tác để xử lý
các tác vụ chạy trong thời gian dài. Ngoài invoke
(hoặc call
) và return
,
coroutine còn thêm suspend
và resume
:
suspend
tạm dừng việc thực thi coroutine hiện tại, lưu tất cả các biến cục bộ.resume
tiếp tục thực thi một coroutine bị tạm ngưng từ vị trí tạm ngưng.
Bạn chỉ có thể gọi các hàm suspend
từ các hàm suspend
khác hoặc
bằng cách sử dụng một hàm tạo coroutine như launch
để bắt đầu một coroutine mới.
Ví dụ sau đây cho thấy một cách triển khai coroutine đơn giản cho một tác vụ giả định chạy trong thời gian dài:
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
Trong ví dụ này, get()
vẫn chạy trên luồng chính nhưng lại tạm ngưng
coroutine trước khi bắt đầu yêu cầu mạng. Khi yêu cầu mạng
hoàn tất, get
sẽ cho coroutine bị tạm ngưng trước đó chạy tiếp thay vì sử dụng
lệnh gọi lại để thông báo cho luồng chính.
Kotlin sử dụng một khung ngăn xếp để quản lý việc hàm nào đang chạy cùng với các biến cục bộ. Khi tạm ngưng một coroutine, khung ngăn xếp hiện tại sẽ được sao chép và lưu lại để sử dụng sau. Khi tiếp tục, khung ngăn xếp sẽ được sao chép ngược trở lại từ vị trí đã lưu và hàm sẽ bắt đầu chạy lại. Mặc dù mã có thể trông giống như một yêu cầu thông thường để chặn theo tuần tự, nhưng coroutine giúp đảm bảo rằng yêu cầu mạng tránh chặn luồng chính.
Dùng coroutine để đảm bảo an toàn cho luồng chính
Coroutine của Kotlin sử dụng trình điều phối (dispatcher) để xác định luồng nào được dùng để thực thi coroutine. Để chạy mã bên ngoài luồng chính, bạn có thể yêu cầu các coroutine trong Kotlin thực hiện tác vụ trên trình điều phối Mặc định hoặc IO. Trong Kotlin, tất cả các coroutine phải chạy trong một trình điều phối, ngay cả khi đang chạy trên luồng chính. Coroutine có thể tự tạm ngưng và trình điều phối sẽ chịu trách nhiệm chạy tiếp coroutine.
Để chỉ định nơi coroutine sẽ chạy, Kotlin cung cấp ba trình điều phối mà bạn có thể sử dụng:
- Dispatchers.Main – Sử dụng trình điều phối này để chạy coroutine trên luồng
Android chính. Bạn chỉ nên sử dụng trình điều phối này để tương tác với giao diện người dùng và
thực hiện tác vụ nhanh. Ví dụ: gọi hàm
suspend
, chạy thao tác khung giao diện người dùng Android và cập nhật đối tượngLiveData
. - Dispatchers.IO – Trình điều phối này được tối ưu hoá để thực hiện I/O đĩa hoặc I/O mạng bên ngoài luồng chính. Ví dụ: sử dụng thành phần Room, đọc hoặc ghi vào tệp và chạy bất kỳ thao tác mạng nào.
- Dispatchers.Default – Trình điều phối này được tối ưu hoá để thực hiện tác vụ nặng về CPU bên ngoài luồng chính. Ví dụ về trường hợp sử dụng: sắp xếp danh sách và phân tích cú pháp JSON.
Tiếp tục ví dụ trước, bạn có thể sử dụng trình điều phối để xác định lại hàm
get
. Bên trong phần chính của get
, hãy gọi withContext(Dispatchers.IO)
để
tạo một khối chạy trên nhóm luồng IO. Mọi mã mà bạn đặt bên trong khối đó
sẽ luôn thực thi thông qua trình điều phối IO
. Do bản thân withContext
là một
hàm tạm ngưng nên hàm get
cũng là một hàm tạm ngưng.
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
Khi sử dụng coroutine, bạn có thể gửi các luồng bằng công cụ kiểm soát chi tiết. Vì
withContext()
cho phép bạn kiểm soát nhóm luồng của mọi dòng mã mà không
cần lệnh gọi lại, nên bạn có thể áp dụng cho các hàm rất nhỏ như đọc
trên cơ sở dữ liệu hoặc thực hiện yêu cầu mạng. Một cách hay là sử dụng
withContext()
để đảm bảo mọi hàm đều an toàn cho luồng chính (main-safe), nghĩa là bạn
có thể gọi hàm từ luồng chính. Bằng cách này, phương thức gọi không
cần suy nghĩ xem nên dùng luồng nào để thực thi hàm.
Trong ví dụ trước, fetchDocs()
thực thi trên luồng chính. Tuy nhiên, hàm này
có thể gọi get
một cách an toàn để thực hiện một yêu cầu mạng ở chế độ nền.
Vì các coroutine hỗ trợ suspend
và resume
, nên coroutine trên luồng chính
sẽ được tiếp tục bằng kết quả get
ngay khi khối withContext
hoàn tất.
Hiệu suất của withContext()
withContext()
không làm tăng thêm mức hao tổn so với phương thức tương đương dựa trên lệnh gọi lại
trong quá trình triển khai. Ngoài ra, trong một số trường hợp, bạn có thể tối ưu hoá lệnh gọi withContext()
ngoài cách triển khai tương đương là triển khai lệnh gọi lại. Ví
dụ: nếu một hàm thực hiện 10 lệnh gọi tới một mạng, bạn có thể yêu cầu Kotlin chỉ chuyển đổi luồng một lần bằng cách sử dụng withContext()
bên ngoài. Sau đó, mặc dù thư viện mạng sử dụng withContext()
nhiều lần, nhưng thư viện này vẫn ở trên cùng một trình điều phối và tránh chuyển đổi luồng. Ngoài ra, Kotlin tối ưu hoá việc chuyển đổi
giữa Dispatchers.Default
và Dispatchers.IO
để tránh việc chuyển đổi chuỗi
bất cứ khi nào có thể.
Khởi chạy coroutine
Bạn có thể khởi chạy coroutin bằng một trong hai cách sau:
launch
khởi động một coroutine mới và không trả về kết quả cho phương thức gọi. Đối với những tác vụ mà bạn chỉ cần khởi chạy và không phải làm gì thêm, bạn có thể khởi chạy bằnglaunch
async
bắt đầu một coroutine mới và cho phép bạn trả về kết quả bằng một lệnh tạm ngưng có tên làawait
.
Thông thường, bạn nên launch
một coroutine mới từ một hàm thông thường, vì hàm thông thường không thể gọi await
. Chỉ sử dụng async
khi ở bên trong
một tệp coroutine khác hoặc khi ở bên trong một hàm tạm ngưng và đang thực hiện
hoạt động phân ly song song.
Phân ly song song
Bạn phải ngừng tất cả các coroutine được khởi chạy trong một hàm suspend
khi
hàm đó quay lại. Vì vậy, có khả năng bạn sẽ cần đảm bảo rằng các coroutine đó
hoàn tất trước khi quay lại. Thông qua cơ chế xử lý đồng thời có cấu trúc (structured concurrency) trong Kotlin, bạn có thể xác định
coroutineScope
để bắt đầu một hoặc nhiều coroutine. Sau đó, hãy sử dụng await()
(đối với một coroutine) hoặc awaitAll()
(đối với nhiều coroutine), bạn có thể đảm bảo rằng các coroutine này đã hoàn tất trước khi quay trở lại từ hàm.
Ví dụ: hãy xác định coroutineScope
có vai trò tìm nạp hai tài liệu
một cách không đồng bộ. Bằng cách gọi await()
trên mỗi tham chiếu trì hoãn, chúng tôi đảm bảo rằng
cả hai thao tác async
đều hoàn tất trước khi trả về một giá trị:
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
Bạn cũng có thể sử dụng awaitAll()
trên các tập hợp, như trong ví dụ sau:
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
Mặc dù fetchTwoDocs()
chạy các coroutine mới bằng async
, nhưng hàm này vẫn sử dụng awaitAll()
để đợi các coroutine đã khởi chạy hoàn tất trước khi quay lại. Tuy nhiên, hãy lưu ý rằng ngay cả khi chúng tôi không gọi awaitAll()
, hàm tạo coroutineScope
cũng không tiếp tục coroutine đã gọi
fetchTwoDocs
, cho đến khi tất cả các coroutine mới đã hoàn tất.
Ngoài ra, coroutineScope
còn phát hiện mọi ngoại lệ mà các coroutine gửi
và đưa trở lại cho phương thức gọi.
Để biết thêm thông tin về việc phân ly song song, hãy xem bài viết Soạn các hàm tạm ngưng.
Các khái niệm về coroutine
CoroutineScope
CoroutineScope
theo dõi mọi coroutine mà nó tạo bằng launch
hoặc async
. Bạn có thể huỷ
tác vụ đang diễn ra (tức là các coroutine đang chạy) bằng cách gọi
scope.cancel()
tại bất kỳ thời điểm nào. Trên Android, một số thư viện KTX cung cấp
CoroutineScope
riêng cho một số lớp trong vòng đời. Ví dụ:
ViewModel
có
viewModelScope
,
và Lifecycle
có lifecycleScope
.
Tuy nhiên, không giống như trình điều phối, CoroutineScope
không chạy coroutine.
viewModelScope
cũng được sử dụng trong các ví dụ ở phần
Tạo luồng chạy ở chế độ nền trên Android thông qua coroutine.
Tuy nhiên, nếu cần tạo CoroutineScope
của riêng mình để kiểm soát vòng đời của coroutine trong một tầng (layer) cụ thể trong ứng dụng, thì bạn có thể tạo
như sau:
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
Phạm vi đã huỷ không thể tạo thêm coroutine. Do đó, bạn chỉ nên
gọi scope.cancel()
khi lớp kiểm soát vòng đời của hàm đó đang bị huỷ bỏ. Khi sử dụng viewModelScope
, lớp
ViewModel
sẽ tự động huỷ
phạm vi cho bạn theo phương thức onCleared()
của ViewModel.
Tác vụ
Job
là một xử lý cho một coroutine. Mỗi coroutine mà bạn tạo bằng launch
hoặc async
sẽ trả về một thực thể Job
xác định riêng
coroutine đó và quản lý vòng đời của coroutine đó. Bạn cũng có thể truyền Job
đến
CoroutineScope
để quản lý vòng đời một cách sâu hơn, như trong ví dụ
sau:
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
CoroutineContext
CoroutineContext
xác định hành vi của một coroutine bằng cách sử dụng nhóm phần tử sau đây:
Job
: Kiểm soát vòng đời của coroutine.CoroutineDispatcher
: Gửi tác vụ tới luồng phù hợp.CoroutineName
: Tên của coroutine, hữu ích cho việc gỡ lỗi.CoroutineExceptionHandler
: Xử lý các ngoại lệ chưa phát hiện được.
Đối với coroutine mới được tạo trong một phạm vi, thực thể Job
mới
sẽ được gán cho coroutine mới và các phần tử CoroutineContext
khác
được kế thừa từ phạm vi chứa coroutine đó. Bạn có thể ghi đè các phần tử
kế thừa bằng cách truyền một CoroutineContext
mới đến hàm launch
hoặc async
. Lưu ý rằng việc truyền Job
đến launch
hoặc async
không có hiệu lực,
vì thực thể mới của Job
luôn được gán cho một coroutine mới.
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine on Dispatchers.Main as it's the scope's default
val job1 = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
// Starts a new coroutine on Dispatchers.Default
val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
Tài nguyên khác về coroutine
Để xem thêm tài nguyên về coroutine, hãy xem các đường liên kết sau: