Chào mừng độc giả đến với Cafedev, nơi chúng tôi chia sẻ kiến thức và kinh nghiệm đa dạng trong lĩnh vực công nghệ. Hôm nay, chúng ta sẽ cùng nhau khám phá thế giới của Kotlin với Cancellation và Timeouts. Trên nền tảng này, chúng ta có thể tận hưởng sức mạnh linh hoạt và kiểm soát tinh tế trong việc quản lý các coroutine, từ việc hủy bỏ đến đặt thời gian chờ. Hãy cùng bắt đầu hành trình lập trình hiệu quả với Kotlin trên Cafedev!

Phần này đề cập đến việc hủy bỏ và đặt thời gian chờ cho coroutine.

1. Hủy bỏ thực hiện coroutine

Trong một ứng dụng chạy lâu dài, bạn có thể cần kiểm soát tinh tế đối với các coroutine nền của mình. Ví dụ, người dùng có thể đã đóng trang web khởi chạy coroutine và hiện không còn cần kết quả nữa, và hoạt động đó có thể được hủy bỏ. Hàm [launch] trả về một [Job] có thể được sử dụng để hủy bỏ coroutine đang chạy:

val job = launch {
    repeat(1000) { i ->
        println("job: I'm sleeping $i ...")
        delay(500L)
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // cancels the job
job.join() // waits for job's completion 
println("main: Now I can quit.")

Bạn có thể xem mã đầy đủ tại đây.
Nó tạo ra đầu ra sau:

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.

Ngay khi main gọi job.cancel, chúng ta không thấy bất kỳ đầu ra nào từ coroutine khác vì nó đã bị hủy bỏ. Cũng có một hàm mở rộng Job là cancelAndJoin kết hợp giữa các lời gọi cancel và Job

2. Hủy bỏ coroutine là sự phối hợp

Hủy bỏ coroutine là sự phối hợp. Mã coroutine phải phối hợp để có thể bị hủy bỏ. Tất cả các hàm tạm dừng trong kotlinx.coroutines đều có thể hủy bỏ. Chúng kiểm tra việc hủy bỏ của coroutine và ném ra CancellationException khi bị hủy bỏ. Tuy nhiên, nếu một coroutine đang làm việc trong một tính toán và không kiểm tra việc hủy bỏ, nó sẽ không thể bị hủy bỏ, như ví dụ dưới đây:

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (i < 5) { // computation loop, just wastes CPU
        // print a message twice a second
        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.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")

Bạn có thể xem mã đầy đủ tại đây.
Chạy nó để thấy rằng nó tiếp tục in “I’m sleeping” ngay cả sau khi bị hủy bỏ cho đến khi công việc hoàn thành tự động sau năm lần lặp.

Cùng vấn đề có thể quan sát được bằng cách bắt CancellationException và không ném lại nó:

val job = launch(Dispatchers.Default) {
    repeat(5) { i ->
        try {
            // print a message twice a second
            println("job: I'm sleeping $i ...")
            delay(500)
        } catch (e: Exception) {
            // log the exception
            println(e)
        }
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")

Bạn có thể lấy mã đầy đủ tại đây.


Mặc dù việc bắt Exception là một mô hình không tốt, vấn đề này có thể xuất hiện một cách tinh tế hơn, như khi sử dụng hàm runCatching, không ném lại CancellationException.

3. Tạo code tính toán có thể hủy bỏ

Có hai cách để làm cho mã tính toán có thể hủy bỏ. Cách đầu tiên là định kỳ gọi một hàm tạm dừng kiểm tra việc hủy bỏ. Có một hàm [yield] là lựa chọn tốt cho mục đích đó. Cách thứ hai là kiểm tra tình trạng hủy bỏ một cách rõ ràng. Hãy thử cách tiếp cận thứ hai.
Thay thế while (i < 5) trong ví dụ trước bằng while (isActive) và chạy lại nó.

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (isActive) { // cancellable computation loop
        // print a message twice a second
        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.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")


Bạn có thể lấy mã đầy đủ tại đây.


Như bạn có thể thấy, bây giờ vòng lặp này đã bị hủy bỏ. isActive là một thuộc tính mở rộng sẵn có bên trong coroutine thông qua đối tượng CoroutineScope

4. Đóng tài nguyên với finally

Các hàm tạm dừng có thể hủy bỏ ném ra CancellationException khi bị hủy bỏ, có thể xử lý theo cách thông thường. Ví dụ, biểu thức try {...} finally {...} và hàm use của Kotlin thực hiện các hành động hoàn thiện của chúng bình thường khi một coroutine bị hủy bỏ:

val job = launch {
    try {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    } finally {
        println("job: I'm running finally")
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")

Bạn có thể lấy mã đầy đủ tại đây.


Cả join và cancelAndJoin đều đợi cho tất cả các hành động hoàn thiện hoàn tất, vì vậy ví dụ trên tạo ra đầu ra sau

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
main: Now I can quit.

5. Chạy khối code non-cancellable

Mọi cố gắng sử dụng một hàm tạm dừng trong khối finally của ví dụ trước đều gây ra CancellationException, vì coroutine chạy mã này đã bị hủy bỏ. Thông thường, điều này không phải là vấn đề, vì tất cả các hoạt động đóng (đóng tệp, hủy bỏ công việc hoặc đóng bất kỳ kênh giao tiếp nào) thường là không chặn và không liên quan đến bất kỳ hàm tạm dừng nào. Tuy nhiên, trong trường hợp hiếm khi bạn cần tạm dừng trong một coroutine đã bị hủy bỏ, bạn có thể bọc mã tương ứng trong withContext(NonCancellable) {...} sử dụng hàm withContext và ngữ cảnh NonCancellable, như ví dụ dưới đây:

val job = launch {
    try {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    } finally {
        withContext(NonCancellable) {
            println("job: I'm running finally")
            delay(1000L)
            println("job: And I've just delayed for 1 sec because I'm non-cancellable")
        }
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")

Bạn có thể lấy mã đầy đủ tại đây.

6. Hết thời gian

Lý do thực tế rõ ràng nhất để hủy bỏ thực hiện một coroutine là vì thời gian thực hiện đã vượt quá một số thời gian chờ. Trong khi bạn có thể theo dõi thủ công tham chiếu đến Job tương ứng và khởi chạy một coroutine riêng để hủy bỏ coroutine đã theo dõi sau một đợi chờ, có một hàm withTimeout sẵn có để thực hiện điều này. Hãy xem ví dụ dưới đây:

withTimeout(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}

Bạn có thể lấy mã đầy đủ tại đây.


Nó tạo ra đầu ra sau:

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

TimeoutCancellationException được ném ra bởi withTimeout là một lớp con của CancellationException. Chúng ta chưa thấy chuỗi stack trace của nó được in trên console trước đó. Điều này là do bên trong một coroutine đã bị hủy bỏ, CancellationException được coi là một lý do bình thường để hoàn thành coroutine. Tuy nhiên, trong ví dụ này, chúng ta đã sử dụng withTimeout ngay trong hàm main.


Vì hủy bỏ chỉ là một ngoại lệ, tất cả các tài nguyên được đóng theo cách thông thường. Bạn có thể bọc mã với timeout trong một khối try {...} catch (e: TimeoutCancellationException) {...} nếu bạn cần thực hiện một số hành động bổ sung cụ thể trên bất kỳ thời gian chờ nào hoặc sử dụng hàm withTimeoutOrNull giống như withTimeout nhưng trả về null khi hết thời gian chờ thay vì ném ra một ngoại lệ:

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")

Bạn có thể lấy mã đầy đủ tại đây.


Không còn có ngoại lệ khi chạy mã này:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null

7. Hết thời chờ và tài nguyên không đồng bộ

Sự kiện hết thời gian chờ trong withTimeout là không đồng bộ đối với mã chạy trong khối của nó và có thể xảy ra bất cứ lúc nào, ngay cả trước khi trả về từ bên trong khối hết thời gian chờ. Hãy nhớ điều này nếu bạn mở hoặc có được một tài nguyên nào đó bên trong khối cần được đóng hoặc giải phóng bên ngoài khối.


Ví dụ, ở đây chúng ta giả mạo một tài nguyên có thể đóng với lớp Resource chỉ giữ theo dõi số lần nó được tạo bằng cách tăng giảm bộ đếm acquired trong hàm close. Bây giờ hãy tạo nhiều coroutines, mỗi coroutine tạo một Resource ở cuối khối withTimeout và giải phóng tài nguyên bên ngoài khối. Chúng ta thêm một độ trễ nhỏ để có khả năng cao là hết thời gian chờ xảy ra ngay khi khối withTimeout đã hoàn thành, điều này sẽ gây ra một rò rỉ tài nguyên.

var acquired = 0

class Resource {
    init { acquired++ } // Acquire the resource
    fun close() { acquired-- } // Release the resource
}

fun main() {
    runBlocking {
        repeat(10_000) { // Launch 10K coroutines
            launch { 
                val resource = withTimeout(60) { // Timeout of 60 ms
                    delay(50) // Delay for 50 ms
                    Resource() // Acquire a resource and return it from withTimeout block     
                }
                resource.close() // Release the resource
            }
        }
    }
    // Outside of runBlocking all coroutines have completed
    println(acquired) // Print the number of resources still acquired
}

Bạn có thể lấy mã đầy đủ tại đây.

Nếu bạn chạy mã trên, bạn sẽ thấy nó không luôn in ra giá trị không, tuy nhiên điều này có thể phụ thuộc vào thời gian của máy bạn. Bạn có thể cần điều chỉnh thời gian chờ trong ví dụ này để thực sự thấy giá trị khác không.

Lưu ý rằng tăng và giảm bộ đếm acquired ở đây từ 10K coroutines hoàn toàn an toàn đối với luồng, vì nó luôn xảy ra từ cùng một luồng, luồng được sử dụng bởi runBlocking. Thêm thông tin về điều này sẽ được giải thích trong chương về ngữ cảnh coroutine.
Để làm việc xung quanh vấn đề này, bạn có thể lưu trữ một tham chiếu đến tài nguyên trong một biến thay vì trả về nó từ khối
withTimeout.

runBlocking {
    repeat(10_000) { // Launch 10K coroutines
        launch { 
            var resource: Resource? = null // Not acquired yet
            try {
                withTimeout(60) { // Timeout of 60 ms
                    delay(50) // Delay for 50 ms
                    resource = Resource() // Store a resource to the variable if acquired      
                }
                // We can do something else with the resource here
            } finally {  
                resource?.close() // Release the resource if it was acquired
            }
        }
    }
}
// Outside of runBlocking all coroutines have completed
println(acquired) // Print the number of resources still acquired


Bạn có thể lấy mã đầy đủ tại đây.
Ví dụ này luôn in ra giá trị không. Tài nguyên không rò rỉ.

Chân thành cảm ơn bạn đã dành thời gian đọc về Kotlin với Cancellation và Timeouts trên Cafedev. Hy vọng rằng thông tin chúng tôi chia sẻ đã giúp bạn hiểu rõ hơn về sức mạnh của ngôn ngữ lập trình này trong việc quản lý coroutine. Đừng ngần ngại khám phá thêm nhiều bài viết hữu ích khác trên Cafedev để nắm bắt những xu hướng công nghệ mới và cập nhật kiến thức của bạn. Hãy tiếp tục đồng hành cùng Cafedev, nơi chia sẻ kiến thức, kết nối cộng đồng và thúc đẩy sự phát triển chung của cộng đồng lập trình.”

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của Cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!