Đăng nhập Tài khoản VIP

Tìm hiểu Suspend Function

Khi tiếp cận Async Programming trong Kotlin thì không ít anh em nhầm lẫn việc chỉ cần đưa keyword suspend trước các function là function đó sẽ chuyển từ blocking sang non-blocking. Thực sự thì khi mới đọc quá thì thấy quá hay nên mình đã thử

sleep()
println("end")
Thread.sleep(2000L)

suspend fun sleep() {
    Thread.sleep(5000L)
}


🎯 Kết quả là vẫn blocking như thường, đọc lại document một chút thì chỉnh lại như sau

fun main() {
    println(Thread.currentThread().name)
    GlobalScope.launch(Dispatchers.Unconfined) {
        println(Thread.currentThread().name)
        sleepAndReturn()
    }
    println("end")
}

suspend fun sleepAndReturn() = withContext(Dispatchers.Default) {
    Thread.sleep(10000L)
}

✅ Mình đưa sleepAndReturn về worker thread thì mọi chuyển đúng ý, mình cố tình đưa logic trong hàm main về main thread, vì nếu khác thread thì quá hiển nhiên


Vậy thì suspend function nó thực hư là thế nào ? Một số web chém tưng bừng, nhiều chữ lắm nhưng đến lúc cần thì quăng cái hình và hô lớn => suspend function là một function có thể pause and resume rồi chuyển qua chủ đề khác => thốn tập 2, vậy thực hư thế nào ????


Tiếp tục research ở một số nguồn uy tín hơn thì thấy trường hợp trực quan nhất để hiểu cái vụ pause và resume

fun main() {
    println(Thread.currentThread().name)

    GlobalScope.launch(Dispatchers.Unconfined){ // anh em lưu ý body trong lamda là một suspend function 
        println(Thread.currentThread().name)
        println("start coroutine")
        delay(2000L) // delay là một suspend function 
        println("end coroutine")
    }
    println("end")
    Thread.sleep(4000L)
}

Ta được kết quả

main
main
start coroutine
end
end coroutine

Process finished with exit code 0

Từ kết quả này có thể thấy rằng sử dụng suspend function trong coroutine (hàm delay) sẽ không làm block thread đang chạy (cụ thể trường hợp này là main thread), khi code chạy đến delay thì bị pause lại và do thread không bị block nên nó chạy ngay tới lệnh println("end") và khi delay 2 giây tới hạn thì nó resume trở lại và chạy tiếp lệnh println("end coroutine") và cuối cùng đợi hết 4 giây thì kết thúc chương trình. Nếu bạn thay delay(2000L) bằng Thread.sleep(2000L) thì lại quay về trường hợp của blocking.


Đến đây thì một số anh em tò mò tại sao suspend function có thể làm vậy ?

Mình có thử decompile 1 suspend function + research thì thấy rằng với mỗi function mà có sử dụng keyword suspend thì khi compile nó chỉnh lại function này như sau

function(param, @NotNull Continuation $completion) {}  

Như vậy Continuation chính là đối tượng chúng ta cần quan tâm để hiểu về cơ chế resume, anh em có thể tự trải nghiệm khi compile nhé. Về cơ bản thì mình hiểu như vầy tại mỗi thời điểm call suspend function thì compile nó sẽ tạo ra tương ứng với 1 label kèm theo data của function đó và được lưu trong Continuation, mỗi khi thực hiện xong logic của hàm đó thì kết quả sẽ được lưu lại để sử dụng nếu cần (state machine) đồng thời label sẽ được gán cho giá trị mới, giá trị này sẽ bằng label của function tiếp theo và lúc này suspend function sẽ được resume lại lúc này label đã là giá trị mới và tương ứng với logic tiếp theo .... cứ như thế cho tới khi chạy hết function.

public inline fun <T> Continuation(
    context: CoroutineContext,
    crossinline resumeWith: (Result<T>) -> Unit
): Continuation<T> =
    object : Continuation<T> {
        override val context: CoroutineContext
            get() = context

        override fun resumeWith(result: Result<T>) =
            resumeWith(result)
    }


Mình copy một đoạn lúc mình test

switch(((<undefinedtype>)$continuation).label) {
case 0:
   ResultKt.throwOnFailure($result);
   String var7 = "process order";
   boolean var3 = false;
   System.out.println(var7);
   ((<undefinedtype>)$continuation).L$0 = email;
   ((<undefinedtype>)$continuation).label = 1;
   var10000 = getUserByEmail(email, (Continuation)$continuation);
   if (var10000 == var6) {
      return var6;
   }
   break;
case 1:
   email = (String)((<undefinedtype>)$continuation).L$0;
   ResultKt.throwOnFailure($result);
   var10000 = $result;
   break;
case 2:
   user = (User)((<undefinedtype>)$continuation).L$1;

Anh em có thể thấy rõ label xử lý trong switch-case, mỗi label sẽ tương tương ứng với một suspend point.

Vậy chốt lại chúng ta sử dụng suspend function để thực hiện những công việc cần thời gian tính toán dài hơi nhưng nhớ là đặt nó trong một coroutine nhé.