Chào mừng bạn đến với Cafedev! Trong bài viết này, chúng ta sẽ khám phá cơ bản về Kotlin với Coroutines. Đối với những ai muốn tìm hiểu về sức mạnh của ngôn ngữ lập trình Kotlin và cách sử dụng Coroutines để tối ưu hóa mã nguồn, đây là nơi đúng. Cafedev sẽ hướng dẫn bạn qua những khái niệm cơ bản và cách sử dụng chúng trong lập trình hàng ngày. Hãy bắt đầu hành trình khám phá Kotlin và Coroutines cùng Cafedev!

Phần này bao gồm các khái niệm cơ bản về coroutine.

1. Coroutine đầu tiên của bạn

Một coroutine là một trường hợp của một tính toán có thể tạm dừng. Nó có ý tưởng tương tự như một luồng, trong ý nghĩa rằng nó chạy một khối mã cùng một lúc với phần còn lại của mã. Tuy nhiên, một coroutine không bị ràng buộc bởi bất kỳ luồng cụ thể nào. Nó có thể tạm dừng thực thi của mình trong một luồng và tiếp tục trong luồng khác.
Coroutine có thể được coi là luồng nhẹ, nhưng có một số sự khác biệt quan trọng khiến việc sử dụng chúng trong thực tế khác biệt rất nhiều so với luồng.
Chạy mã dưới đây để có coroutine đầu tiên của bạn:

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello") // main coroutine continues while a previous one is delayed
}


Bạn sẽ thấy kết quả sau:
textHelloWorld!

Hãy phân tích những gì mã này thực hiện.

launch là một coroutine builder. Nó khởi chạy một coroutine mới cùng một lúc với phần còn lại của mã, mà tiếp tục hoạt động độc lập. Đó là lý do tại sao Hello đã được in trước.

delay là một hàm tạm dừng đặc biệt. Nó tạm dừng coroutine trong một khoảng thời gian cụ thể. Việc tạm dừng một coroutine không chặn luồng cơ bản, mà cho phép các coroutine khác chạy và sử dụng luồng cơ bản cho mã của chúng.

runBlocking cũng là một coroutine builder, nó làm cầu nối giữa thế giới không phải là coroutine của một fun main() thông thường và mã với coroutine bên trong cặp dấu ngoặc nhọn runBlocking { ... }. Điều này được làm nổi bật trong IDE bằng mẹo this: CoroutineScope ngay sau dấu ngoặc nhọn mở của runBlocking.

Nếu bạn loại bỏ hoặc quên runBlocking trong mã này, bạn sẽ gặp lỗi khi gọi launch, vì launch chỉ được khai báo trong phạm vi của CoroutineScope:
Plain TextUnresolved reference: launch

Tên của runBlocking có nghĩa là luồng chạy nó (trong trường hợp này là luồng chính) bị blocked trong suốt cuộc gọi, cho đến khi tất cả các coroutine bên trong runBlocking { ... } hoàn thành thực thi. Bạn thường thấy runBlocking được sử dụng như vậy ở cấp độ cao nhất của ứng dụng và khá hiếm khi bên trong mã thực sự, vì luồng là tài nguyên đắt đỏ và chặn chúng là không hiệu quả và thường không mong muốn.

Cấu trúc concurrency

Coroutines tuân theo nguyên tắc cấu trúc concurrency có nghĩa là coroutine mới chỉ có thể được khởi chạy trong một [CoroutineScope] cụ thể làm giới hạn thời gian tồn tại của coroutine. Ví dụ trên cho thấy [runBlocking] thiết lập phạm vi tương ứng và đó là lý do tại sao ví dụ trước đó đợi cho đến khi World! được in sau một giây chờ đợi và chỉ thoát sau đó.
Trong một ứng dụng thực tế, bạn sẽ khởi chạy nhiều coroutines. Cấu trúc concurrency đảm bảo rằng chúng không bị mất và không rò rỉ. Một phạm vi bên ngoài không thể hoàn thành cho đến khi tất cả các coroutine con của nó hoàn thành. Cấu trúc concurrency cũng đảm bảo rằng mọi lỗi trong mã được báo cáo đúng cách và không bao giờ bị mất.

2. Rút gọn hàm

Hãy rút gọn khối mã bên trong launch { ... } thành một hàm riêng. Khi bạn thực hiện rút gọn “Extract function” trên mã này, bạn sẽ có một hàm mới với từ khóa suspend. Đây là hàm suspending function đầu tiên của bạn. Suspending functions có thể được sử dụng bên trong coroutines giống như các hàm thông thường, nhưng tính năng bổ sung của chúng là chúng có thể, lần lượt, sử dụng các suspending functions khác (như delay trong ví dụ này) để tạm dừng thực thi của một coroutine.

fun main() = runBlocking { // this: CoroutineScope
    launch { doWorld() }
    println("Hello")
}

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

Bạn có thể xem mã đầy đủ tại đây.

3. Phạm vi builder

Ngoài phạm vi coroutine được cung cấp bởi các builders khác nhau, bạn có thể khai báo phạm vi riêng của mình bằng cách sử dụng builder coroutineScope. Nó tạo ra một phạm vi coroutine và không hoàn thành cho đến khi tất cả các coroutine con đã hoàn thành.
Cả runBlocking và coroutineScope có vẻ giống nhau vì cả hai đều đợi cho đến khi body của chúng và tất cả các coroutine con của nó hoàn thành. Sự khác biệt chính là phương thức runBlocking chặn
luồng hiện tại để chờ đợi, trong khi coroutineScope chỉ đơn giản là tạm dừng, giải phóng luồng cơ bản để sử dụng cho các mục khác. Vì sự khác biệt đó, runBlocking là một hàm thông thường và coroutineScope là một hàm suspending.

Bạn có thể sử dụng coroutineScope từ bất kỳ hàm suspending nào. Ví dụ, bạn có thể di chuyển việc in HelloWorld đồng thời vào một hàm suspend fun doWorld():

fun main() = runBlocking {
    doWorld()
}

suspend fun doWorld() = coroutineScope {  // this: CoroutineScope
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}

Bạn có thể xem mã đầy đủ tại đây.

Mã này cũng in ra:
textHelloWorld!

4. Phạm vi builder và concurrency

Một builder coroutineScope có thể được sử dụng bên trong bất kỳ hàm suspending nào để thực hiện nhiều hoạt động đồng thời. Hãy khởi chạy hai coroutine đồng thời bên trong hàm doWorld suspending:

// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
    doWorld()
    println("Done")
}

// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
    launch {
        delay(2000L)
        println("World 2")
    }
    launch {
        delay(1000L)
        println("World 1")
    }
    println("Hello")
}

Bạn có thể xem mã đầy đủ tại đây.
Cả hai đoạn mã bên trong các khối launch { ... } thực thi đồng thời, với World 1 được in trước, sau một giây từ khi bắt đầu, và World 2 được in tiếp theo, sau hai giây từ khi bắt đầu. Một coroutineScope trong doWorld chỉ hoàn thành sau khi cả hai đều hoàn thành, vì vậy doWorld trả về và cho phép chuỗi ký tự Done được in ra chỉ sau đó:
Hello

World 1

World 2

Done

5. Một công việc rõ ràng

Một builder coroutine launch trả về một đối tượng Job là một cánh cửa để theo dõi coroutine được khởi chạy và có thể được sử dụng để đợi rõ ràng cho việc hoàn thành của nó. Ví dụ, bạn có thể đợi cho đến khi coroutine con hoàn thành và sau đó in chuỗi “Done”:

val job = launch { // launch a new coroutine and keep a reference to its Job
    delay(1000L)
    println("World!")
}
println("Hello")
job.join() // wait until child coroutine completes
println("Done") 

Bạn có thể lấy toàn bộ mã nguồn tại đây.
Mã này tạo ra:

Hello
World!
Done

6. Coroutines khá nhẹ

Coroutines ít tốn tài nguyên hơn so với các luồng trên JVM. Mã nguồn khiến JVM hết bộ nhớ sử dụng luồng có thể được thể hiện bằng coroutines mà không gặp vấn đề giới hạn tài nguyên. Ví dụ, đoạn mã dưới đây khởi chạy 50,000 coroutines khác nhau mỗi coroutine đợi 5 giây và sau đó in một dấu chấm (‘.’) mà tiêu tốn rất ít bộ nhớ:

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(50_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}

Bạn có thể lấy toàn bộ mã nguồn tại đây.

Nếu bạn viết chương trình tương tự bằng cách sử dụng luồng (loại bỏ runBlocking, thay launch bằng thread, và thay delay bằng Thread.sleep), nó sẽ tiêu tốn rất nhiều bộ nhớ. Tùy thuộc vào hệ điều hành, phiên bản JDK, và cài đặt của nó, nó sẽ entweder throw một lỗi out-of-memory hoặc khởi động luồng chậm sao cho không bao giờ có quá nhiều luồng đang chạy đồng thời.

Cảm ơn bạn đã đọc bài viết về Kotlin with Coroutines basics trên Cafedev. Chúng tôi hy vọng rằng bạn đã có cái nhìn rõ ràng về sức mạnh của Kotlin và sự linh hoạt của Coroutines trong quá trình phát triển ứng dụng. Cafedev luôn đồng hành cùng cộng đồng lập trình viên, mang đến kiến thức và trải nghiệm giá trị. Hãy tiếp tục theo dõi để không bỏ lỡ những bài viết mới và những kiến thức hữu ích khác từ Cafedev. Chúng tôi mong rằng bạn sẽ tiếp tục đồng hành và chia sẻ cùng chúng tôi trên hành trình lập trình.”

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của Cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!