Cùng học Kotlin Coroutine, phần 4: Job, Join, Cancellation and Timeouts
Bài đăng này đã không được cập nhật trong 4 năm
1. Job - một element trong coroutine context
Như chúng ta đã biết ở phần 3: Trong coroutine context có một element là Job
giữ nhiệm vụ nắm giữ thông tin về lifecycle của coroutine, cancel coroutine, .... Mỗi khi chúng ta launch một coroutine thì nó trả về một đối tượng Job
này.
val job = GlobalScope.launch { // launch a new coroutine and keep a reference to its Job
delay(5000L)
println("World!")
}
Ở những mục tiếp theo của bài viết này, chúng ta sẽ được giới thiệu một số property và method hay dùng liên quan đến đối tượng job này.
2. Hàm join() - hãy đợi coroutine chạy xong đã!
Chúng ta có thể sử dụng đối tượng Job
để thực hiện một số method có sẵn trong mỗi coroutine. Ví dụ ở đây mình sử dụng hàm join()
. Khi một coroutine gọi hàm join()
này thì tiến trình phải đợi coroutine này chạy xong task của mình rồi mới chạy tiếp. Ví dụ:
fun main() = runBlocking {
val job = GlobalScope.launch { // launch a new coroutine and keep a reference to its Job
delay(5000L)
println("World!")
}
println("Hello,")
job.join() // wait until child coroutine completes
println("Kotlin")
}
Output:
22:07:20 I/System.out: Hello
22:07:25 I/System.out: World
22:07:25 I/System.out: Kotlin
Nhìn output ta có thể dễ dàng thấy khi tiến trình chạy xong dòng code in ra từ "Hello,"
thì nó gặp lệnh join() và nó không tiếp tục chạy xuống dòng code bên dưới để in tiếp từ "Kotlin"
mà chờ coroutine chạy xong task để in ra từ "World"
trước cái đã. Đó là công dụng của hàm join()
3. Hàm cancel() - hủy bỏ một coroutine
Để dừng và hủy bỏ một coroutine đang chạy. Ta có thể dùng method cancel()
của biến Job
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // cancels the job
println("main: Now I can quit.")
}
Output:
I'm sleeping 0 …
I'm sleeping 1 …
I'm sleeping 2 …
main: I'm tired of waiting!
main: Now I can quit.
Ở đoạn code trên, mình cho phóng một coroutine và bảo nó in ra câu "I'm sleeping ..."
cứ mỗi 500 ms và in đủ 1000 lần như vậy. Và đoạn code dưới, mình cho tiến trình delay 1300 ms trước khi cancel con coroutine mình đã phóng. Kết quả là sau 1300 ms, nó mới chỉ in được có 3 câu "I'm sleeping ..."
mà nó đã bị hủy bỏ nên không in tiếp được nữa
4. Những lưu ý khi hủy bỏ một coroutine
4.1. Coroutine cancellation is cooperative
Thử dùng hàm cancel()
để hủy bỏ coroutine trong đoạn code sau:
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) {
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // cancels the job
println("main: Now I can quit.")
}
Output:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
Ôi, thật bất ngờ!. Đoạn code trên, mình cũng cho phóng một coroutine và bảo nó in ra câu "I'm sleeping ..."
cứ mỗi 500 ms và in đủ 5 lần như vậy. Tuy nhiên sau 1300 ms, mình đã gọi hàm cancel()
để hủy bỏ corotine đó, tức là nó chỉ có đủ thời gian để in ra được 3 câu "I'm sleeping ..."
nhưng thực tế output cho thấy nó vẫn chạy bất chấp và in ra đủ 5 câu "I'm sleeping ..."
=))
Đó là vì quá trình hủy bỏ coroutine có tính hợp tác (Coroutine cancellation is cooperative). Một coroutine khi bị cancel thì nó sẽ chỉ set
lại một property có tên là isActive
trong đối tượng Job
từ true
thành false
(job.isActive = false
), còn tiến trình của nó đang chạy thì sẽ vẫn chạy bất chấp cho đến hết mà không bị dừng lại. Vậy tại sao, ở đoạn code trong phần 2, tiến trình của coroutine lại được hủy bỏ thành công. Đó là vì hàm delay(500L)
ngoài chức năng delay thì bản thân nó cũng có một chức năng có thể check coroutine này còn sống hay không, nếu không còn sống (job.isActive == false
) nó sẽ hủy bỏ tiến trình của coroutine đó ngay và luôn. Không chỉ riêng hàm delay()
mà tất cả các hàm suspend function trong package kotlinx.coroutines
đều có khả năng check này.
Vậy chúng ta đã biết thêm một property tuyệt vời của đối tượng Job
là isActive
. Nó giúp chúng ta kiểm tra xem coroutine đã bị cancel hay chưa. Thử áp dụng nó vào code để kịp thời ngăn chặn tiến trình của coroutine khi đã có lệnh hủy bỏ coroutine đó xem nào
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // Điều kiện i < 5 đã được thay bằng isActive để ngăn chặn coroutine khi nó đã bị hủy
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // cancels the job
println("main: Now I can quit.")
}
Output:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
Tuyệt vời!. Nếu như không có biến isActive
thì vòng lặp while
sẽ làm cho coroutine in ra vô số câu "I'm sleeping ..."
. Nhờ có điều kiện isActive
nên chúng ta đã ngăn chặn được coroutine sau khi nó đã bị hủy bỏ, khiến nó chỉ có thể in ra 3 câu "I'm sleeping ..."
.
4.2. Sử dụng khối finally để close resource ngay cả khi coroutine đã bị hủy bỏ.
Nếu tiến trình của một coroutine bị hủy bỏ thì ngay lập tức nó sẽ tìm đến khối finally
để chạy code trong đó. Chúng ta có thể sử dụng đặc điểm này để tranh thủ close hết các resource trước khi coroutine đó chính thức bị khai tử
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
} finally {
// Tranh thủ close resource trong này đi nha :D
println("I'm running finally")
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // cancels the job
println("main: Now I can quit.")
}
Output:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
I'm running finally
Như chúng ta thấy trong kết quả output, ngay cả khi coroutine bị dừng không thể tiếp tục in ra những câu "I'm sleeping ..."
và đã chạy đến dòng code cuối để in ra câu "main: Now I can quit."
mà nó vẫn cố gắng chạy vào khối finally
để in ra câu "I'm running finally"
trước khi trút hơi thở cuối cùng
4.3. Coroutine vẫn có thể chết trong khối finally
Bây giờ, thử để hàm delay()
bên trong khối finally của đoạn code trên thử xem:
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
} finally {
println("I'm running finally")
delay(1000L) // hàm delay được thêm vào khối finally
println("Print me please!")
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // cancels the job
println("main: Now I can quit.")
}
Output:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
I'm running finally
What!. Tại sao coroutine chạy vào khối finally
in ra được câu "I'm running finally"
nhưng lại không thể tiếp tục chạy xuống code dưới để in ra câu "Print me please!"
. Tất cả tại thằng hàm delay()
. Như mình đã nói ở trên, hàm delay()
nó riêng hay tất cả hàm suspend function nói chung có khả năng check xem coroutine còn sống không. Nếu nó đã chết thì tiến trình lập tức bị dừng lại ngay khi chạy vào hàm delay()
này. Vậy thì câu "Print me please!"
tất nhiên sẽ không được in ra rồi =))
4.4. Làm cho coroutine bất tử
Vậy giả sử bây giờ chúng ta muốn nó thực thi bất chấp tất cả dòng code trong khối finally
thì làm cách nào?. Vẫn có cách nhé. Một element thuộc coroutine context có tên là NonCancellable
sẽ giúp ta thực hiện điều này.
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
} finally {
withContext(NonCancellable) { // Nhờ có em NonCancellable mà anh được phép chạy bất chấp đấy
println("I'm running finally")
delay(1000L)
println("I'm non-cancellable")
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // cancels the job
println("main: Now I can quit.")
}
Output:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
I'm running finally
I'm non-cancellable
Ở đoạn code trên, có 2 kiến thức lạ là NonCancellable
và hàm withContext()
.
- Hàm
withContext()
có tác dụng điều chỉnh lại context của coroutine. Cụ thể trước đó coroutine lúc mới được sinh ra thì bản thân nó default là Cancellable (có thể hủy bỏ được) nhưng khi coroutine chạy được một lúc rồi mình lại muốn nó đổi context thànhNonCancellable
(không thể hủy bỏ được). Khi đó hàmwithContext()
sẽ giúp chúng ta thực hiện việc điều chỉnh đó. Công dụng khác của hàmwithContext()
có thể kể đến như một coroutine thực thi task dưới background thread (Dispatchers.IO
) và sau khi xong task thì cho nó chạy tiếp trên main threadwithContext(Dispatchers.Main)
để update UI chẳng hạn. Mình sẽ nói nhiều hơn về hàmwithContext()
ở các bài sau nhé . NonCancellable
là một element trong tập context của coroutine. Công dụng của nó là khiến cho coroutine trở nên bất tử, không thứ gì có thể khiến nó dừng lại cho đến khi nó hoàn thành xong task nhé =))
5. Timeout - cho coroutine chết bằng cách hẹn giờ
Chúng ta có thể ra lệnh cho coroutine: "Nhà ngươi hãy làm task này cho ta trong vòng 10 giây, nếu hết 10 giây mà ngươi vẫn làm chưa xong thì hãy chết đi!". Hàm withTimeout(truyền_vào_khoảng_thời_gian_đơn_vị_ms)
sẽ cho ta cái quyền lực như vậy.
fun main() = runBlocking {
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
}
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
WTF!. Sao lại gặp Exception
!. Đúng vậy. Hàm withTimeout()
khá là gắt khi nó thấy hết thời gian timeout mà vẫn chưa thấy coroutine xong task nó sẽ throw TimeoutCancellationException
. Điều này đồng nghĩa với việc sẽ không có Exception
nào xảy ra nếu coroutine hoàn thành task trước khi hết thời gian timeout.
Chúng ta có hàm withTimeoutOrNull(truyền_vào_khoảng_thời_gian_đơn_vị_ms)
có công dụng như hàm withTimeout()
nhưng bớt gắt hơn. Thay vì throw TimeoutCancellationException
thì bản thân hàm withTimeoutOrNull()
sẽ return về một biến null
khi hết thời gian timeout rồi mà coroutine vẫn chưa xong task.
fun main() = runBlocking {
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done" // will get cancelled before it produces this result
}
println("Result is $result") // Biến result sẽ null
}
Output:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null
Kết luận
Kết thúc phần 4, hy vọng bạn đã nắm rõ các kiến thức liên quan đến việc cancel một coroutine. Cảm ơn các bạn đã theo dõi bài viết này. Hy vọng các bạn sẽ tiếp tục theo dõi những phần tiếp theo.
Nguồn tham khảo:
https://kotlinlang.org/docs/reference/coroutines/basics.html
https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html
Đọc lại những phần trước:
Cùng học Kotlin Coroutine, phần 1: Giới thiệu Kotlin Coroutine và kỹ thuật lập trình bất đồng bộ
Cùng học Kotlin Coroutine, phần 2: Build first coroutine with Kotlin
Cùng học Kotlin Coroutine, phần 3: Coroutine Context và Dispatcher
Đọc tiếp phần 5: Cùng học Kotlin Coroutine, phần 5: Async & Await
All rights reserved