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: Job
và Deferred
Đ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 Job
và Deferred
: 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
hayDispatchers.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.
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ộtsuspend 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 Coroutines và RxJava (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. |
Tiêu chí | Kotlin Coroutines | RxJava |
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ó. |
Operator | – Kotlin 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 |