Coroutines — khái niệm chính, Job và Dispatchers trong Kotlin

Tác giả: IT Sectr Đã đăng: 2026-03-16 Thời gian đọc: 8 phút

Coroutines là các luồng nhẹ trong Kotlin cho lập trình bất đồng bộ, có sẵn qua thư viện kotlinx.coroutines. Theo JetBrains Kotlin Documentation, 2026, Coroutines cho phép tạm dừng thực thi hàm mà không chặn luồng, không giống như Thread truyền thống. Coroutines chạy trên một nhóm luồng giới hạn, khiến chúng nhẹ hơn hàng nghìn lần so với luồng gốc. Kotlin Coroutines được tích hợp đầy đủ với Android Jetpack, Retrofit, Room và các thư viện phổ biến khác của hệ sinh thái Android.

Những Điểm Chính

  • Coroutines — luồng Kotlin nhẹ cho mã bất đồng bộ không chặn
  • Hàm suspend — hàm có thể tạm dừng và tiếp tục mà không chặn luồng
  • Dispatcher xác định nhóm luồng để thực thi coroutine
  • Job — bộ mô tả coroutine với hỗ trợ hủy và theo dõi trạng thái
  • CoroutineScope quản lý vòng đời của coroutines và việc hủy chúng khi hoàn thành

Coroutines Kotlin là gì

Coroutines là một cơ chế lập trình bất đồng bộ trong Kotlin, được triển khai trong thư viện kotlinx.coroutines. Không giống như các luồng của hệ điều hành, coroutines không bị ràng buộc vào một luồng cụ thể: chúng có thể tạm dừng trên một luồng và tiếp tục trên một luồng khác. Một luồng đơn có thể thực thi hàng nghìn coroutines, chuyển đổi giữa chúng với chi phí tối thiểu.

Coroutines xuất hiện trong Kotlin 1.3 (2018) như một tính năng thử nghiệm và trở nên ổn định trong Kotlin 1.5 (2021). Coroutines giải quyết vấn đề callback hell tương tự như async/await, nhưng cung cấp API phong phú hơn: kênh (Channel), Flow, xử lý ngoại lệ trong hệ thống phân cấp Job và tích hợp trực tiếp với Android Lifecycle.

Theo JetBrains (2025), mỗi coroutine tiêu thụ khoảng 100 byte bộ nhớ so với 1+ MB cho một luồng gốc. Điều này cho phép chạy hàng triệu coroutines trong một ứng dụng duy nhất mà không có rủi ro OutOfMemoryError. Chính sự nhẹ nhàng của coroutines khiến chúng trở thành công cụ ưa thích cho bất đồng bộ trong Android.

Coroutines hoạt động bên trong như thế nào

Mỗi coroutine Kotlin được biên dịch thành một máy trạng thái thông qua Continuation Passing Style (CPS). Trình biên dịch thêm một tham số Continuation ẩn vào mỗi hàm suspend. Continuation chứa điểm tiếp tục và tất cả các biến cục bộ. Khi một coroutine tạm dừng, runtime lưu Continuation và khi nó tiếp tục, runtime khôi phục nó trên bất kỳ luồng khả dụng nào từ nhóm Dispatcher.

Hàm suspend: tạm dừng và tiếp tục

suspend là một từ khóa Kotlin đánh dấu hàm có thể tạm dừng. Hàm như vậy chỉ có thể được gọi từ một hàm suspend khác hoặc từ một coroutine. Bên trong một hàm suspend, bạn có thể gọi các hàm suspend khác theo bất kỳ thứ tự nào và mỗi điểm gọi là một điểm tạm dừng tiềm năng.

Cơ chế rất đơn giản: khi một hàm suspend gọi một hàm suspend khác, nó tạm dừng tại điểm đó, giải phóng luồng. Sau khi hàm được gọi hoàn thành, runtime tiếp tục thực thi từ vị trí đã lưu. Đây được gọi là hủy hợp tác (cooperative cancellation) — không có luồng nào bị chặn.

  • Tạm dừng — coroutine giải phóng luồng mà không chặn nó
  • Tiếp tục — coroutine tiếp tục từ nơi nó đã tạm dừng
  • Luồng — một coroutine có thể tạm dừng trên luồng A và tiếp tục trên luồng B
  • Ngoại lệ — được xử lý qua try/catch như trong mã đồng bộ

Quan trọng: hàm suspend không phải là bất đồng bộ theo mặc định. Thứ tự thực thi vẫn tuần tự nếu không sử dụng launch hoặc async. suspend chỉ cho phép hàm tạm dừng mà không chặn luồng và trở thành một phần của ngữ cảnh coroutine. Continuation Passing Style là một mô hình biên dịch trong đó mỗi hàm suspend nhận một callback Continuation ẩn và trình biên dịch tạo ra một máy trạng thái để quản lý các lần tạm dừng và tiếp tục.

CoroutineScope và tương tranh có cấu trúc

CoroutineScope là một ngữ cảnh xác định vòng đời của coroutines. Tất cả coroutines phải được khởi chạy trong một scope. Khi một scope bị hủy (ví dụ: khi Activity kết thúc), tất cả các coroutine con của nó tự động bị hủy. Điều này ngăn chặn rò rỉ tác vụ nền. Android Jetpack cung cấp các scope có sẵn cho mỗi thành phần: viewModelScope cho ViewModel và lifecycleScope cho Activity và Fragment, tự động bị hủy khi thành phần tương ứng bị phá hủy.

Tương tranh có cấu trúc (Structured Concurrency) là một nguyên tắc đảm bảo rằng một coroutine sẽ không hoàn thành cho đến khi tất cả các coroutine con của nó hoàn thành. Hệ thống phân cấp Job tạo thành một cây: một coroutine gốc tạo ra job cha, các con tạo ra job con. Việc hủy job cha lan truyền đến tất cả các con. Structured Concurrency là một khác biệt cơ bản giữa coroutines và luồng.

ScopeNơi sử dụngHủy bỏ
GlobalScopeChỉ cho tác vụ daemonKhông tự động hủy
viewModelScopeAndroid ViewModelKhi ViewModel bị xóa
lifecycleScopeAndroid Activity/FragmentKhi lifecycle bị phá hủy
coroutineScopeBên trong hàm suspendKhi job cha bị hủy

SupervisorJo để xử lý lỗi

Job thông thường hủy tất cả siblings khi một coroutine con thất bại. SupervisorJob là một ngoại lệ: lỗi trong một coroutine con không ảnh hưởng đến các coroutine khác. Điều này quan trọng khi nhiều tác vụ độc lập được thực thi song song và một trong số chúng có thể gặp lỗi mà không cần hủy các tác vụ khác.

Dispatchers và trình xây dựng coroutine

Dispatchers xác định coroutines được thực thi trên luồng nào. Dispatchers.Main — luồng UI chính của Android. Dispatchers.IO — nhóm cho các hoạt động chặn (mạng, đĩa). Dispatchers.Default — cho các tác vụ tốn CPU. Dispatchers.Unconfined — bắt đầu trong luồng hiện tại nhưng không đảm bảo ở lại trên đó. Chọn Dispatcher phù hợp rất quan trọng cho hiệu suất: một tác vụ IO trên Default sẽ chặn nhóm tính toán, trong khi tác vụ CPU trên IO sẽ tạo ra các luồng không cần thiết.

withContext — một hàm để chuyển đổi Dispatcher bên trong coroutine. Ví dụ, một hàm suspend phân tích JSON có thể chuyển sang Dispatchers.Default để tính toán và quay lại Dispatchers.Main để cập nhật UI. withContext là trình xây dựng được sử dụng nhiều nhất trong phát triển Android.

Ba trình xây dựng coroutine chính

launch — khởi chạy một coroutine, trả về Job, không trả về kết quả (fire-and-forget). async — khởi chạy một coroutine, trả về Deferred từ đó có thể lấy kết quả qua await. runBlocking — chặn luồng hiện tại để thực thi coroutine (chỉ cho kiểm thử và hàm main). Lựa chọn trình xây dựng phụ thuộc vào kịch bản: launch phù hợp cho các sự kiện và cập nhật, async cho các tác vụ có kết quả, runBlocking chỉ cho kiểm thử hoặc điểm vào.

Ví dụ mã với coroutines trong Kotlin

Hãy xem xét ba kịch bản thực tế: một coroutine cơ bản với launch, một cuộc gọi song song với async và xử lý lỗi với SupervisorJob.

Khởi chạy coroutine với launch

viewModelScope.launch khởi chạy một coroutine trong ngữ cảnh ViewModel. Khi ViewModel bị xóa, coroutine tự động bị hủy.

kotlin
class ProfileViewModel : ViewModel() {
    fun loadUser() {
        viewModelScope.launch(Dispatchers.IO) {
            val user = api.fetchUser()
            withContext(Dispatchers.Main) {
                showUser(user)
            }
        }
    }
}

Yêu cầu song song với async

coroutineScope với async khởi chạy ba yêu cầu song song. Kết quả được thu thập qua .await(). Nếu bất kỳ yêu cầu nào thất bại, tất cả đều bị hủy.

kotlin
suspend fun loadDashboard(): Dashboard = coroutineScope {
    val user = async { api.fetchUser() }
    val posts = async { api.fetchPosts() }
    val stats = async { api.fetchStats() }
    Dashboard(user.await(), posts.await(), stats.await())
}

Xử lý lỗi với SupervisorJob

SupervisorJob cho phép mỗi coroutine hoàn thành độc lập. Lỗi trong một yêu cầu không hủy các yêu cầu khác.

kotlin
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scope.launch {
    try { api.fetchUsers() } catch (e: Exception) { log(e) }
}
scope.launch {
    try { api.fetchPosts() } catch (e: Exception) { log(e) }
}

Coroutines vs luồng: so sánh và kịch bản

Luồng (Thread) là một nguyên thủy của hệ điều hành. Mỗi luồng có ngăn xếp riêng (~1 MB) và yêu cầu một cuộc gọi hệ thống để tạo và chuyển đổi. Coroutines là một nguyên thủy ngôn ngữ, không bị ràng buộc với hệ điều hành. Chúng sử dụng Continuation để lưu trạng thái và chuyển đổi ở cấp độ runtime mà không cần cuộc gọi hệ thống.

  • Bộ nhớ — luồng ~1 MB, coroutine ~100 byte. Chênh lệch 10.000 lần
  • Tạo — luồng ~1 µs syscall, coroutine ~0,01 µs ở cấp JVM
  • Chuyển đổi — luồng ~0,1 µs (syscall), coroutine ~0,001 µs (continuation)
  • Tối đa — hàng nghìn luồng vs hàng triệu coroutine trên mỗi thiết bị
  • Hủy bỏ — luồng không thể bị hủy từ bên ngoài (Thread.stop không còn được dùng), coroutine có thể qua Job.cancel()

Theo Google (2025), sử dụng coroutines thay vì luồng giảm mức tiêu thụ bộ nhớ cho các tác vụ nền trong ứng dụng Android từ 90–95%. Tất cả các thư viện Android hiện đại (Retrofit, Room, WorkManager) đều có hỗ trợ coroutine tích hợp thông qua các hàm suspend. Ktor (framework máy khách HTTP của JetBrains) cũng được xây dựng hoàn toàn trên coroutines, cung cấp các hàm suspend cho mỗi yêu cầu mà không cần API callback. Room hỗ trợ coroutines thông qua các hàm suspend trong DAO, cho phép thực thi truy vấn cơ sở dữ liệu mà không chặn luồng chính.

Khi nào sử dụng luồng thay vì coroutines

Luồng vẫn cần thiết cho mã gốc qua JNI, các cuộc gọi chặn CPU-intensive không giới hạn thời gian (kết xuất video, mô phỏng) và khi tích hợp với thư viện C. Đối với mọi thứ khác — coroutines.

Câu hỏi thường gặp

Coroutine khác với luồng như thế nào?

Coroutine là một đơn vị công việc có thể tạm dừng chạy trên một luồng hiện có. Luồng là một tài nguyên hệ thống với ngăn xếp riêng. Coroutines nhẹ hơn luồng hàng nghìn lần và không chặn tài nguyên khi tạm dừng.

Dispatchers.IO là gì và nó khác với Default như thế nào?

Dispatchers.IO được thiết kế cho các hoạt động I/O chặn (mạng, tệp) và có thể tạo luồng mới khi cần. Dispatchers.Default có một nhóm kích thước cố định (số lõi CPU) cho các tính toán tốn CPU.

Làm thế nào để hủy một coroutine đang chạy?

Job.cancel() hủy coroutine và tất cả các con của nó. Để kiểm tra hủy bên trong coroutine, sử dụng ensureActive() — nó ném CancellationException nếu coroutine bị hủy.

Coroutines có thể được sử dụng với RxJava không?

Có — thông qua thư viện kotlinx-coroutines-rx3. Nó cung cấp các hàm awaitSingle, awaitFirst và các hàm khác để chuyển đổi Observable/Single thành các hàm suspend và ngược lại qua flowable.

Flow trong coroutines là gì?

Flow là một luồng dữ liệu bất đồng bộ nguội, tương đương với RxJava Observable trong coroutines. Flow phát ra các giá trị tuần tự và kết thúc với một ngoại lệ hoặc thành công. Nó hỗ trợ map, filter, catch và các toán tử khác.

Tổng kết

  • Coroutines — luồng Kotlin nhẹ với tạm dừng không chặn qua Continuation Passing Style
  • suspend — từ khóa để đánh dấu các hàm có thể tạm dừng
  • Dispatchers quản lý nhóm luồng: Main, IO, Default tương ứng
  • CoroutineScope liên kết vòng đời coroutine với một thành phần (Activity, ViewModel)
  • launch khởi chạy coroutine không có kết quả, async/await — có kết quả
  • Structured Concurrency đảm bảo hủy có thứ bậc các coroutine con
  • Coroutines vs luồng — coroutines nhẹ hơn 10.000 lần và là tiêu chuẩn cho Android

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm