Kotlin Coroutines - Android Mastery by Dan tech
Kotlin Coroutines - Android Mastery by Dan tech

Android Thread: Kiến Thức Kotlin Coroutines

Trong chuỗi bài viết về Threading trong Android mình đã đề cập về nhiều khái niệm: Android Thread, Looper, RxJava, Backpressure RxJava … Bây giờ là đến lúc tìm hiểu một công cụ mạnh mẽ mới – Kotlin Coroutines

Kotlin Coroutines?

Kotlin – Ngôn ngữ lập trình; routine – lịch trình; co-routines – nhiều lịch trình phối hợp cùng nhau

Kotlin Coroutines là một bộ thư viện được viết bằng ngôn ngữ Kotlin, giúp Lập trình viên sử dụng, quản lý các luồng logic (lịch trình) bất đồng bộ với nhau một cách hiệu quả.

Các khái niệm khi sử dụng Kotlin Coroutines

CoroutineScope

Là nguồn gốc, động cơ để kích hoạt việc khởi chạy 1 Coroutine. Hãy tưởng tượng CoroutineScope giống như một factory nơi khởi tạo ra các Coroutine và quản lý vòng đời của chúng.

Trong một ứng dụng Kotlin bạn có thể tự tạo CoroutineScope cho riêng mình hoặc sử dụng GlobalScope – Một CoroutineScope với tầm hoạt động trên toàn app, khi sử dụng đến GlobalScope cần cẩn thận để tránh chiếm tài nguyên và gây ra Memory Leaks.

Trong một ứng dụng Android, bạn có thể tự tạo CoroutineScope cho riêng từng trường hợp để sử dụng, sử dụng GlobalScope hoặc là sử dụng các built-in CoroutineScope mà Android đã cung cấp sắn: lifecycleScope của Activity, Fragment; viewModelScope của ViewModel. Các built-in CoroutineScope này sẽ có vòng đời riêng, gắn liền với các Component của chúng (Activity, Fragment, ViewModel) giúp bạn thuận tiện hơn trong việc quản lý bộ nhớ và tránh phí phạm tài nguyên dễ gây Memory Leaks.

val customCoroutineScope = CoroutineScope(EmptyCoroutineContext)
customCoroutineScope.launch {
  Log.d("KtCoroutines", "Hello coroutines")
}
viewModelScope.launch {
  Log.d("ViewModelScope", "Hello coroutines")
}
lifecycleScope.launch {
  Log.d("LifecycleScope", "Hello coroutines")
}

Job

Job trong Kotlin Coroutines là đại diện cho 1 Coroutine được tạo ra từ CoroutineScope.

Có 2 loại Job thường được dùng mặc định trong Kotlin Coroutines: JobDeferred

Điểm giống nhau: Cả 2 đều dùng để mô tả 1 Coroutine tạo bởi CoroutineScope. Chứa dữ liệu về trạng thái của Coroutine đang chạy. Có thể được cancel khi cần thiết.

Điểm khác biệt lớn nhất giữa JobDeferred : Job mô tả một tác vụ được thực thi nhưng không quan trọng đến kết quả trả về (fire and forget), Deferred là một tác vụ được thực thi, và chờ đợi kết quả trả về (fire and wait for result)


CoroutineContext, Dispatchers

CoroutineContext

Context: Bối cảnh

CoroutineContext được hiểu là bối cảnh, môi trường để thực thi 1 Coroutine.

Trong CoroutineContext chứa nhiều dữ liệu: Dispatcher, Parent Job, Coroutine Name, .. Trong phạm vi bài viết này hãy tập trung vào Dispatchers bạn nhé.

Dispatchers

Là kẻ quyết định Coroutine sẽ được thực thi trên Thread Pool nào. Bạn lưu ý đây là Thread Pool chứ không phải Thread nhé. Hãy lấy ví dụ sau để chứng minh

viewModelScope.launch(Dispatchers.IO) {
    while (true) {
        Log.d("DispatchersTest", Thread.currentThread().name)
        delay(1000)
    }
}

Quan sát bạn sẽ thấy Thread name được in ra màn hình sẽ được thay đổi, không giữ nguyên giá trị như cũ. Điều này chứng tỏ việc resume của Coroutine sau khi suspend ở hàm delay(1000) đã chuyển logic của Log.d sang một Thread mới. Tuy nhiên, Thread mới này vẫn thuộc quy hoạch Thread Pool của Dispatchers.IO

Kotlin cung cấp cho ứng dụng một số Dispatchers mặc định mà ta có thể lựa chọn.

  • Dispatchers.Default: Là Dispatcher dùng để tạo các Coroutine Job thực thi trên Background Thread. Các task này được khuyến khích, quy định là các tác vụ tính toán, tốn nhiều tài nguyên CPU – (CPU-bound opertaion): tính toán đệ quy, tiền lãi ngân hàng, biến động thị trường, …
  • Dispatchers.IO: Là Dispatcher dùng để tạo các Coroutine Job thực thi trên môi trường Background Thread. Các task được chạy trên IO được khuyến nghị liên quan đến IO operations (thực thi ghi / đọc file, network, …)
  • Dispatchers.Main: Đại diện cho main thread trong ứng dụng. Các Coroutine được thực thi bởi Dispatchers.Main luôn chạy trên 1 Main Thread duy nhất.
  • Dispatchers.Unconfined: Đặc biệt, các Coroutine Job được tạo ra bởi Unconfined sẽ không xác định được môi trường chúng sẽ được thực thi sau khi resume từ 1 suspend. Điều này có nghĩa, tại thời điểm resume hệ thống quản lý Kotlin Coroutines sẽ tìm kiếm một Thread được free gần nhất để tiếp tục thực thi, không giới hạn là Dispatchers.IO Dispatchers.Main hay Dispatchers.Default.
viewModelScope.launch(Dispatchers.Unconfined) {
    while (true) {
        Log.d("DispatchersTest", Thread.currentThread().name)
        delay(1000)
    }
}

Hãy dùng ví dụ trên cho các loại Dispatchers mà bạn muốn sử dụng để hiểu rõ hơn về cơ chế xác định Thread của Dispatchers.

Một điều lưu ý mà không ai nói cho bạn biết:

Dispatchers.IO và Dispatchers.Default chia sẻ chung một số Thread trong ThreadPool. Điều này giúp cho việc switch context giảm bớt tỷ lệ janking, và tăng khả năng tối ưu hiệu năng.

Nguồn: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-i-o.html


Suspend Function

suspend function là một loại function đặc biệt của Kotlin Coroutines. Đúng như cái tên của nó, suspend function có khả năng được suspend (gián đoạn, dừng lại) và tiếp tục resume sau đó để tiếp tục thực thi logic.

  • suspend function chỉ có thể được thực thi bên trong một suspend function khác.
fun normalFunction() {
  delay(1000) // -> error
  newSuspendFunction() // -> error
  
  coroutineScope.launch {
    newSuspendFunction() // -> not error
  }
}

suspend fun newSuspendFunction() {
  Log.d("Running new suspend function")
  anotherSuspendFunction() // -> not error
}

suspend fun anotherSuspendFunction() {
  delay(1000) // -> not error
}

val userApi: UserApi by lazy { get<UserApi>() }
// Real usecase của suspend function
suspend fun fetchFullUserProfile(userId: String): FullUserProfile {
  val remoteUserProfile = async { userApi.getUser(userId) }.await()
  val userSetting = async { userApi.getUserSetting(userId) }.await()
  return FullUserProfile(remoteUserProfile, userSetting)
}

So sánh Kotlin Coroutines và RxJava

Mối liên hệ giữa Kotlin CoroutinesRxJava (Câu hỏi phỏng vấn)

1. Tận dụng OS Thread của thiết bị và thực thi Logic trên đó.
2. Đều dùng cơ chế Thread Pool để tiết kiệm tài nguyên máy.
3. Hỗ trợ cơ chế thuận tiện Switch Thread giữa các môi trường khác nhau.
Giống nhau giữa Kotlin CoroutinesRxJava
Tiêu chíKotlin CoroutinesRxJava
Quản lý đa luồng (Concurrency Management)suspending

Khi 1 logic chạy trên Kotlin Coroutines, logic đó có thể được suspend (ngưng đọng lại, gián đoạn lại) và trả Thread về Thread Pool, đợi đến khi cần thiết sẽ tiếp tục thực thi sau.
blocking

Khi một task chạy trên RxJava, nó sẽ độc chiếm toàn bộ Thread, giả sử có 1 tác vụ chờ trên Thread thì RxJava sẽ chiếm Thread đó, chờ đợi cho đến khi thực thi xong mới trả Thread về Thread Pool.
Đơn vị thực thi (Job/Task Unit)Coroutine

– 1 Coroutine có thể thực thi được trên nhiều OS Thread khác nhau, tại nhiều khoản thời gian khác nhau.

– Đồng thời 1 OS Thread cũng có thể phục vụ lần lượt nhiều Coroutine khác nhau

Tất cả nhờ cơ chế suspending
Thread

– Mỗi Java Thread chỉ thực thi trên 1 OS Thread nhất định trong suốt lifecycle của nó.
OperatorKotlin Coroutines tiếp cận vấn đề này bằng một cách khác. Mỗi Coroutine là 1 Job riêng biệt, và Developer được toàn quyền thao tác, xử lý lên chúng. Không cần thông qua toán tử.

– Sử dụng async, await và logic của lập trình viên để thao tác lên các luồng dữ liệu.
RxJava có hỗ trợ nhiều loại toán tử khác nhau (operator) giúp thao tác dữ liệu giữa nhiều data stream (Observable Object) khác nhau.

– Sử dụng zip, flatMap, concatMap, merge, … để thao tác lên các luồng dữ liệu
Khác biệt giữa Kotlin CoroutinesRxJava