Coroutines trong Kotlin


#1

Một trong những điểm đặc biệt đáng chú ý nhất trong Kotlin mà bạn cần chú ý thì đó chính là coroutines ! Vậy coroutines thực chất là gì ?

Rất lâu trong thế giới lập trình đã tồn tại khái niệm về thread, mình cũng không nói nhiều về thread, nhưng có điểm quan trọng với thread là nó tiêu tốn resource để khởi tạo ở mức OS, cho nên một ứng dụng kiểu như web app khi handle cỡ ngàn request là cần phải cung cấp resource cho nó.

Cùng với sự nặng nề từ thread, một khái niệm mới được sinh ra để handle được nhiều request với ít resource hơn là Actor , tiêu biểu chính là Erlang được sử dụng trong Whatsapp, (bạn có thể đọc về sự thần kỳ của whatsapp tại đây. Nếu là java, thì đó là Akka , nổi bật nhất thông qua Scala, đây là một phần khá hay mình xin dành trong một bài viết khác. Chủ yếu ở đây các actor tạo ra các process và các process này liên lạc với nhau thông qua message bất đồng bộ.

Thời gian sau đó, cùng với sự ra đời của Golang đã mang tới một khái niệm gọi là goroutines rất nổi tiếng với việc chạy song song cùng lúc nhiều coroutines với chi phí rẻ hơn nhiều so với thread. Và thật may mắn cho chúng ta vì bản thân Kotlin cũng implemented khái niệm này nhưng không mặc định. Giờ chúng ta cùng nhau tìm hiểu coroutines trong Kotlin nhé

Coroutines là gì ?
Coroutines được gọi là light-weight threads, vậy chính xác thì light-weight chỗ nào ? Đó chính là viêc coroutines không map tới native-thread mà thread điểm yếu là cần resource để quản lý các kiểu, từ đó coroutines tiêu tốn ít resource hơn so với thread và được quản lý bởi app, tóm tắt như sau :

Coroutines and the threads both are multitasking. But the difference is that threads are managed by the OS and coroutines by the users.

image
Để thêm phần phức tạp :smiley: bạn có thể tham khảo hình phía trên về coroutines. Ở đây điểm quan trọng chính là tính unblocking giữa thread chính với các coroutines. Unblocking là khái niệm rất hay trong các ngôn ngữ lập trình hiện đại đều có, nhiều nhất là Javascript/NodeJS nhờ đó mà tăng tốc độ lên nhiều lần. Trở lại hình trên thì điểm đặc biệt chính là coroutines có thể gọi lẫn nhau một cách trực tiếp thay vì thông qua message như mô hình actor.

Các loại coroutines:
Coroutines không phải là đặc sản của Golang hay Kotlin nha các bạn, nó có trong rất nhiều các ngôn ngữ khác và có 02 loại chính : stackess và stackful - nghe giống EJB hell nhỉ :smiley: Việc này liên quan chủ yếu tới việc bản thân coroutines có dùng stack để callback hay không, việc stackful sẽ phải map tới một native-thread và Kotlin implemented based on stackless không dùng stack

Dài dòng và loằng ngoằng lý thuyết về coroutines xin kết lại bằng đoạn quảng cáo coroutines trên Koltinglang như sau :

One can think of a coroutine as a light-weight thread. Like threads, coroutines can run in parallel, wait for each other and communicate. The biggest difference is that coroutines are very cheap, almost free: we can create thousands of them, and pay very little in terms of performance. True threads, on the other hand, are expensive to start and keep around. A thousand threads can be a serious challenge for a modern machine.

Cài đặt và sử dụng:
Như đã lén lút nói ở trên, coroutines không được mặc định trong Kotlin, mà vì vậy các bạn cần enable coroutines trước khi sử dụng với tấm nhãn experimental rất nhà quê (các bạn nên làm quen với experimental vì nhiều thứ hay ho trong Kotlin hay dán nhãn này lắm :P) . Đừng ngại ngùng thêm đoạn sau trong build.gradle nhé anh em:

kotlin {
    experimental {
        coroutines 'enable'
    }
}
dependencies {
    ...
    compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.27.0"
}

Sau khi Gradle download các thể loại, chúng ta cuối cùng cũng được sờ đến code:

fun main(args:Array<String>){
    println("START VNKotlin Basic - Coroutines")
    launch {
        delay(1000)
        println("Hello from Corountines")
    }
    print("END VNKotlin Basic - Coroutines")
}

Kotlin định nghĩa launch và async cho phép bạn coroutines trong đó, khác nhau thì async cho phép trả về kết quả còn launch thì không
Chạy đoạn code này ra kết quả :

START VNKotlin Basic - Coroutines
END VNKotlin Basic - Coroutines

Coroutines của tui ddaau? Lừa nhau à ?! :face_with_symbols_over_mouth:
Sếp có thể lừa bạn còn code thì không, như có nói ở trên , coroutines không map tới main thread nên đoạn delay(1000) sẽ không chạy xong thì chương trình cũng đã kết thúc, gọi là chưa đến chợ đã tiêu hết tiền vậy :smiley: Theo tinh thần Apple thì đó không phải lỗi, đó là tính năng, chúng ta âm thầm cập nhật đoạn code lại như sau :

fun main(args:Array<String>){
    println("START VNKotlin Basic - Coroutines")
    launch {
        delay(1000)
        println("Hello from Corountines")
    }
    Thread.sleep(2000)
    print("END VNKotlin Basic - Coroutines")
}

START VNKotlin Basic - Coroutines
Hello from Corountines
END VNKotlin Basic - Coroutines

Đoạn ở trên minh họa cho việc Kotlin implemented coroutines sử dụng stackless. Tiếp theo sau là đoạn code minh họa cho tính hiệu quả của coroutines:

fun increaseByThread(){
    val c = AtomicInteger()
    for (i in 1..1_000_000)
        thread(start = true) {
            c.addAndGet(i)
        }
    println("By Thread : ${c.get()}")
}

fun increaseByCoroutines(){
    val c = AtomicInteger()
    for (i in 1..1_000_000)
        launch {
            c.addAndGet(i)
        }
    println("By Coroutines : ${c.get()}")
}
fun main(args:Array<String>){
    val threadTimeMeasure = measureTimeMillis {
        increaseByThread()
    }

    val coroutineTimeMeasure = measureTimeMillis {
        increaseByCoroutines()
    }

    println("Compare Thread vs Coroutines : ${threadTimeMeasure} <> ${coroutineTimeMeasure}")
}

02 đoạn code trên giống nhau hoàn toàn chỉ khác ở trên dùng thread, ở dưới dùng coroutines, và đây là kết quả :

Compare Thread vs Coroutines : 40766 <> 1093

Thời giạn chênh lệch tính bằng miliseconds!
Lập trình viên thấy ngon hơn là khoái, nhưng lúc nhìn lại thấy kết quả 02 hàm chạy có gì đó sai sai, thread thì luôn cộng đúng là 1784293664 , còn coroutines thì hên xui ! Đó là vì một số coroutines có thể chạy chưa xong trước khi hàm main kết thúc, thiệt là, ok fix tiếp :smiley:

fun increaseByCoroutines(){
    val deferred = (1..1_000_000).map { n ->
       async {
           n
       }
    }
    runBlocking {
        val sum = deferred.sumBy { it.await() }
        println("By Coroutines : ${sum}")
    }
}

Giờ thì 02 kết quả đã giống nhau, nhưng mà nhìn đoạn code nhiều hơn lúc đầu. Như đã nói ở trên async là hàm cho phép chạy coroutines trả về kết quả, cụ thể ở đây sẽ trả về giá trị n từ 1 -> 1000000, sau đó giá trị được coroutines xử lý sẽ trả về qua hàm await(), hàm này ở đâu ra? Thì ra async trả về một instance của Deffered , await() là hàm từ instance này! Tới đây, tiếp tục gọi hàm sumBy của instance Deffered để tính tổng lại các giá trị coroutines trả về là xong, nhưng bạn sẽ gặp lỗi compile :

Suspend functions are only allowed to be called from a coroutine or another suspend function

Lỗi trên là do coroutines cần phải được suspend lại chờ các coroutines khác kết thúc, mà việc suspend này chỉ được thực hiện trong một coroutines mà thôi, cho nên đoạn tính tổng cần phải được để trong một coroutines nữa, ở đây là runBlocking {…} (tương tự launch{…}). Ngoài ra bạn cũng có thể định nghĩa đoạn code bên trong async {…} thành một hàm riêng biệt, nhưng để sử dụng được trong corountines bạn cần thêm từ khóa suspend phía trước nếu không muốn gặp lỗi như trên

suspend fun workload(n: Int): Int {
    delay(1000)
    return n
}

fun increaseByCoroutines(){
    val deferred = (1..1_000_000).map { n ->
        async {
            workload(n)
        }
    }
    launch {
        val sum = deferred.sumBy { it.await() }
        println("By Coroutines : ${sum}")
    }
}

Vậy là xong ! Code dài hơn nhưng chạy nhanh hơn nhỉ :smiley:
Chắc nhiều bạn sẽ hỏi sao coroutines có lợi vậy mà không xài coroutines hết đi, bỏ thread đi blah blah … vậy thì tốc độ ứng dụng sẽ nhanh hơn nhiều. Đúng là coroutines hay thật, nhưng thread đã dùng rất lâu và rất nhiều trong các thư viện cũng như framework, việc thay đổi hoàn toàn không thể thực hiện ngày một ngày hai, đó cũng là vấn đề gặp phải với Actor của Scala. Kết lại thì Kotlin cung cấp cho bạn những công cụ tốt nhất có thể, vấn đề là các bạn sẽ vận dụng nó như thế nào thôi. Chúc vui

Source code : https://github.com/vnkotlin/basic/tree/master/coroutines

  • Dở ẹc
  • Thường
  • Hay ho
  • Đỉnh kao

0 người bình chọn


#2

Cảm ơn bác đã giới thiệu về Coroutines.
Cho mình hỏi thêm một chút là đối với lập trình Android dùng Kotlin thì có recommend là khi nào nên dùng Coroutine không? Hoặc cụ thể là 1 case nào đó trong Kotlin thì nên dùng Coroutine không ?


#3

Hi bạn,
Bạn dùng coroutines thay cho thread ở hầu hết các case mà ko có share data đều ok, nếu có share data giữa các coroutines với nhau thì bạn phải đổi qua cơ chế channel (mình trình bày ở bài viết sau). Ví dụ như trường hợp app android bạn lấy data từ một server , hiển thị lên UI đồng thời tiếp tục chạy các tác vụ khác, nếu bác không dùng thread hoặc coroutines thì UI sẽ khựng một chút để chờ load data, còn nếu dùng thread thì tốn chi phí hơn so với dùng coroutines. Điểm khác nhau cơ bản nữa là dùng coroutines thì các hàm bên trong nó phải là suspend

Chúc vui


Channels trong Coroutines - p1
#4

Cám ơn bác đã reply !
Thực ra về phần xử lý bất đồng bộ nói chung và việc lấy data từ server về nói riêng có rất nhiều framework, library mạnh mẽ hỗ trợ việc này. Có thể kể đến như RxJava, RxAndroid, etc…
Mình nghĩ hầu hết mọi người đều đã từng dùng Rx và thấy rõ sức mạnh của nó. Android thuần tuý trước đây nó cũng có support sẵn mấy thằng như AsynTask chẳng hạn, nên cái mình muốn đi sâu một chút là thay cho việc dùng Rxjava thì mình dùng coroutines sẽ có điểm lợi nào ở trong những trường hợp nào?
Ví dụ như bác nói trong trường hợp lấy data từ server về thì dùng coroutines cho tiện. Có bác khác thì lại nói dùng rxJava ở đây là tiện nhất rồi ,blabla.
Theo mình hiện tại coroutines vẫn chỉ là mới bắt đầu chưa support được mạnh mẽ, nên nó cũng chỉ xử lý một vài trường hợp không quá phức tạp . Rxjava nó support rất nhiều các phương thức cũng như toán tử giúp transform mạnh mẽ, Ko biết hiểu vậy có đúng không nữa.
Mình hỏi có thô lỗ quá không , nếu bác biết thì share cho anh em ạ :slight_smile:

Đây có một bài so sánh về rxJava và coroutines


#5

hi bạn,

Coroutines về căn bản cũng là tận dụng cơ chế unblocking trong java , nhưng làm nó đơn giản hơn và tích hợp sẵn. Về Rxjava so với coroutines thì mình không có ý kiến vì thấy nó cùng xử lý gần giống nhau với callback và suspend function, chỉ là Kotlin hướng chúng ta sử dụng coroutines có sẵn thay vì một thư viện bên ngoài. Điểm mạnh của Kotlin là tính multi platform của nó, tức là cùng cơ chế coroutines, bạn dùng trên frontend với js hay android lẫn backend thì đều giống nhau. Mình cũng chưa xài coroutines đủ nhiều để so sánh với rxjava, tuy nhiên mình thấy coroutines có vẻ dễ đọc hơn :smiley: . Nếu bạn đã có dự án xài rxjava, mình nghĩ bạn ko nên đổi qua coroutines, nhưng nếu một dự án mới hoàn toàn thì đó là cơ hội để thử coroutines

Chúc vui