+3

Thread.sleep() and kotlinx.coroutines.delay()

Mayfest2023

Chào mọi người, trong lập trình Android, khi gặp một yêu cầu liên quan đến delay hoặc dừng một task vụ sau một khoảng thời gian chờ nhất định, chúng ta thường nghĩ đến 2 phương án là Thread.sleep()kotlinx.coroutines.delay() .

Vậy 2 cơ chế này có gì khác nhau và khi nào mình nên sử dụng chúng. Hôm nay mình sẽ xin chia sẻ tới mọi người ý hiểu của mình.

1. Khái niệm

kotlinx.coroutines.delay()Thread.sleep() đều là cơ chế giới thiệu độ trễ hoặc tạm dừng trong quá trình thực thi chương trình, nhưng chúng được sử dụng trong các ngữ cảnh khác nhau và có các đặc điểm khác nhau.

1.1. kotlinx.coroutines.delay()

kotlinx.coroutines.delay() là một suspending function được cung cấp bởi thư viện Kotlin coroutines. Nó thường được sử dụng trong các asynchronous programming khi làm việc với các coroutine, nơi bạn muốn tạm dừng việc thực thi một coroutine mà không chặn luồng chính.

1.2. Thread.sleep()

Thread.sleep() là một static method được cung cấp bởi lớp Thread trong Java (cũng có thể sử dụng được trong Kotlin) và được sử dụng để tạm dừng quá trình thực thi của luồng hiện tại.

Thread.sleep() không chỉ tạm dừng việc thực thi một coroutine mà còn chặn toàn bộ luồng, nghĩa là các luồng khác trong ứng dụng sẽ không thể tiếp tục công việc của chúng trong suốt thời gian Sleep.

2. Ví dụ triển khai

2.1. kotlinx.coroutines.delay()

fun main(args: Array<String>) {
    runBlocking {
            run()
        }
    }
}

suspend fun run() {
    coroutineScope {
        val timeInMillis = measureTimeMillis {
            val mainJob = launch {
                //Job 0
                launch {
                    print("A->")
                    delay(1000)
                    print("B->")
                }
                //Job 1
                launch {
                    print("C->")
                    delay(2000)
                    print("D->")
                }
                //Job 2
                launch {
                    print("E->")
                    delay(500)
                    print("F->")
                }

                //Main job
                print("G->")
                delay(1500)
                print("H->")
            }

            mainJob.join()
        }

        val timeInSeconds =
            String.format("%.1f", timeInMillis/1000f)
        print("${timeInSeconds}s")
   }
}

Luồng chạy sẽ như nhau:

Main Job sẽ chạy và sẽ bị tạm dừng bởi delay.

Tiếp đó là Job 0 -> Job 1 -> Job 2, 3 jobs đều khởi động cùng một lúc và bị delay với thời gian tương ứng trong từng job.

Tiếp theo, task có delay() time ngắn nhất thì chạy xong trước, theo sau là các công việc tiếp theo hoàn thành. Độ trễ dài nhất là 2s.

Kết quả là:

G->A->C->E->F->B->H->D->2.0s

or

G->A->C->E->F->B->H->D->2.1s

Nhìn vào kết quả, tại sao là 2.1s mà không phải là delay() time dài nhất 2.0s. Vì các bạn có thể thấy khoảng time từ khi Main Job bắt đầu đến khi delay của Job1 là 0.01s. Có một số lần chạy, khoảng time đó quá nhỏ nên kết quả có thể là 2s.

2.2. Thread.sleep() on Dispatchers.Main

fun main(args: Array<String>) {
    runBlocking {
            run()
        }
    }
}


suspend fun run() {
    coroutineScope {
        val timeInMillis = measureTimeMillis {
            val mainJob = launch {
                //Job 0
                launch {
                    print("A->")
                    delay(1000)
                    print("B->")
                }
                //Job 1
                launch {
                    print("C->")
                    Thread.sleep(2000)
                    print("D->")
                }
                //Job 2
                launch {
                    print("E->")
                    delay(500)
                    print("F->")
                }

                //Main job
                print("G->")
                delay(1500)
                print("H->")
            }

            mainJob.join()
        }

        val timeInSeconds =
            String.format("%.1f", timeInMillis/1000f)
        print("${timeInSeconds}s")
    }

}

Luồng chạy sẽ như nhau:

Main Job sẽ chạy và sẽ bị tạm dừng bởi delay.

Tiếp đó là Job 0 -> Job 1. Job sẽ bị suspended. Tuy nhiên Thread.sleep(2000) chạy ở Job 1, nó sẽ block Main Thread trong 2s. Job2 trong thời gian dó không thực thi gì cả.

Sau 2s, D sễ được in ra, tiếp đó là E của Job2. Job 2 sẽ bị suspend. Vì Main Job và Job 1 có suspend time nhỏ hơn 2s, vậy nên sau đó nó sẽ được in ra ngay lập tức. Lúc này Job 0 sẽ được chạy trước vì có time delay bé hơn.

Cuối cùng là 0.5s, Job2 sẽ tiếp tục chạy và hoàn thành in ra F.

Kết quả là: G->A->C->D->E->B->H->F->2.5s

Timestamp #1 (sau 0 second)

  • Main job và Job 0 start and suspended.

  • Job 1 start và blocks Thread

Timestamp #2 (sau 2 seconds)

  • Job 1 hoàn thành

  • Job 2 start và suspended.

  • Job 0 và Main Job tiếp tục và hoàn thành.

Timestamp #3 (after 0.5 seconds)

  • Job 3 tiếp tục và hoàn thành.

Khoảng thời gian tổng dao động trong 2.5s.

2.3. Thread.sleep() on Dispatchers.Default/IO or Dispatchers.Default/Default

fun main(args: Array<String>) {
    withContext(Dispatchers.Default) {
        run()
    }
}


suspend fun run() {
    coroutineScope {
        val timeInMillis = measureTimeMillis {
            val mainJob = launch {
                //Job 0
                launch {
                    print("A->")
                    delay(1000)
                    print("B->")
                }
                //Job 1
                launch {
                    print("C->")
                    Thread.sleep(2000)
                    print("D->")
                }
                //Job 2
                launch {
                    print("E->")
                    delay(500)
                    print("F->")
                }

                //Main job
                print("G->")
                delay(1500)
                print("H->")
            }

            mainJob.join()
        }

        val timeInSeconds =
            String.format("%.1f", timeInMillis/1000f)
        print("${timeInSeconds}s")
    }

}

Kết quả là:

G->A->C->E->F->B->H->D->2.0s

Kết quả khá tương tự với ví dụ sử dụng kotlinx.coroutines.delay()

Khi Dispatchers.Default hoặc Dispatchers.IO được sử dụng, nó được hỗ trợ bởi một nhóm luồng. Mỗi lần chúng ta gọi launch{}, một worker thread khác được created/used.

Ví dụ, đây là các Worker thread đang được sử dụng:

Main Job - DefaultDispatcher-worker-1

Job 0 - DefaultDispatcher-worker-2

Job 1 - DefaultDispatcher-worker-3

Job 2 - DefaultDispatcher-worker-4

Để xem luồng nào hiện đang chạy, bạn có thể sử dụng println("Run ${Thread.currentThread().name}")

Vì vậy, Thread.sleep() thực sự chặn luồng đó, nhưng chỉ chặn DefaultDispatcher-worker-3. Các công việc khác vẫn có thể được tiếp tục chạy vì chúng nằm trên các luồng khác nhau.

Timestamp #1 (after 0 second)

  • Main Job, Job 0, Job 1 và Job 2 start.

  • Main Job, Job 0 và Job2 bị suspended.

  • Job 1 bị block trên Thread của nó, ở đây là DefaultDispatcher-worker-3.

Timestamp #2 (after 0.5 second)

  • Job 2 tiếp tục và hoàn thành

Timestamp #3 (after 1 second)

  • Job 0 tiếp tục và hoàn thành

Timestamp #4 (after 1.5 seconds)

  • Main Job tiếp tục và hoàn thành

Timestamp #5 (after 2 seconds)

  • Job 1 tiếp tục và hoàn thành

Bởi vì mỗi công việc chạy trên một luồng khác nhau, công việc có thể được bắt đầu vào những thời điểm khác nhau. Vì vậy, đầu ra của A, C, E, G có thể là ngẫu nhiên. Như vậy, bạn thấy trình tự các task có thể khác với trình tự trong Exampe 1 ở phần trên.

3. Conclusion

Thread.sleep() chặn luồng gọi nó còn kotlinx.coroutines.delay() thì không.

Chỉ nên Thread.sleep() để kiểm tra xem tôi đã đặt đúng task chạy dài vào Background Thread chưa.

Cuối cùng, kotlinx.coroutines.delay() là phương pháp được khuyến nghị để tạo độ trễ trong một coroutine mà không chặn luồng giao diện người dùng.

Cảm ơn các bạn đã theo dõi. Hẹn gặp mọi người ở bài viết tiếp theo.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí