Note: see the link below for the English version of this article.

https://duongnt.com/fire-and-forget-vie

Fire and forget với coroutine trong Kotlin

Trong một số tình huống, chúng ta cần thực hiện một xử lý nào đó mà không cần đợi nó chạy xong, cũng không quan tâm tới kết quả nó trả về. Ta gọi những xử lý như vậy là có tính chất fire and forget.

Trong Kotlin, coroutine là cách phổ biến nhất để thực hiện fire and forget. Trong bài hôm nay, chúng ta sẽ xem xét một số phương án khác nhau và xem tại sao có phương án chạy đúng, có phương án chạy sai.

Tóm tắt các phương án

Đây là một phương án chạy đúng như ta muốn. Nhưng nó sử dụng GlobalScope, đây là tính năng thường không được khuyên dùng.

GlobalScope.launch(Dispatchers.IO) {
	// code fire and forget tại đây
}

Còn đây là cách mọi người hay dùng. Tuy nhiên thực ra nó không khác cách một là bao.

CoroutineScope(Dispatchers.IO).launch {
    // code fire and forget tại đây
}

đây là một phương án sai, dù nhìn nó không khác mấy cách thứ hai.

coroutineScope {
    launch(Dispatchers.IO) {
        // code fire and forget tại đây
    }
}

Điểm khác biệt giữa các phương án

Điểm chung của cả ba phương án trên là ta dùng hàm launch để tạo coroutine, sau đó thực hiện xử lý của mình bên trong coroutine đó. Rồi ta trả về ngay từ hàm main mà không đợi coroutine chạy xong. Tiếp theo đây ta sẽ xem kỹ từng phương án.

Sử dụng GlobalScope

Đây là kết quả khi ta chạy code demo. Có thể thấy là ta kết thúc hàm superAwesomeFeature trước khi xuất xong các metric.

Start the application
Start doing some work
Finish doing some work
Start exporting metrics
Finish exporting metrics
Stop the application

GlobalScope là một singleton có implement interface CoroutineScope. Nó đặt context của mình là một context rỗng.

Thông thường, ta không nên dùng GlobalScope vì một số lý do dưới đây.

  • GlobalScope không có vòng đời xác định. Vì thế coroutine chạy bằng GlobalScope dễ sống lâu hơn code tạo ra chúng và dẫn tới memory leak.
  • Vì hàm GlobalScope.launch không sử dụng structured concurrency nên việc xử lý lỗi trong coroutine trở nên phức tạp hơn.
  • Vì không có scope cụ thể nên việc quản lý tài nguyên cũng là khó khăn hơn.

Nhưng nếu để ý kỹ thì những nhược điểm ở trên đều là những điều ta không cần quan tâm, hoặc phải chấp nhận khi sử dụng fire and forget. Ta muốn coroutine tiếp tục chạy kể cả khi code tạo chúng đã trả về. Và ta không quan tâm tới việc chúng gặp lỗi.

Đúng là ta cần có cách để dừng một coroutine nếu nó đã chạy quá lâu. Nhưng fire and forget là một trong những trường hợp hiếm mà ta nên sử dụng GlobalScope.

Sử dụng hàm CoroutineScope

Đây là kết quả khi ta chạy code demo. Có thể thấy là ta kết thúc hàm superAwesomeFeature trước khi xuất xong các metric.

Start the application
Start doing some work
Finish doing some work
Start exporting metrics
Finish exporting metrics
Stop the application

Cần phân biệt hàm CoroutineScope và interface CoroutineScope. Hàm này tạo một object với kiểu là ContextScope. Đây là một internal class có implement interface CoroutineScope. Sau đó ta dùng hàm launch để tạo coroutine, rồi trả về một Job để quản lý coroutine. Tuy nhiên, ở đây ta không hề dùng đến Job đó.

Đây là cách mọi người thường dùng để thực hiện fire and forget để không cần dùng tới GlobalScope. Nhưng về bản chất, không có nhiều sự thay đổi ở đây.

  • Vì ta vứt bỏ Job trả về từ hàm launch nên thực ra ta vẫn không thể quản lý coroutine.
  • Và vì ta trả về ngay sau khi chạy coroutine nên ta vẫn phải để tâm tới việc xử lý lỗi hay kết thúc coroutine chạy lâu.

Trong thực tế, tôi vẫn thường dùng phương án này bởi nó giúp tôi không phải dùng tới @OptIn(DelicateCoroutinesApi::class). Tuy nhiên, ta cần nhớ rằng phương án này là tương đương với sử dụng GlobalScope.

Tại sao coroutineScope không thực hiện fire and forget?

Đây là kết quả khi ta chạy code demo. Có thể thấy là ta phải đợi code xuất metric chạy xong rồi mới kết thúc được hàm superAwesomeFeature.

Start the application
Start doing some work
Start exporting metrics
Finish exporting metrics
Finish doing some work
Stop the application

Một điểm không hay trong code coroutine của Kotlin là nó sử dụng cùng một tên hoặc những tên rất giống nhau cho những khái niệm khác nhau. Chúng ta có interface CoroutineScope, hàm CoroutineScope, và bây giờ lại có hàm suspending với tên gọi coroutineScope.

Hàm coroutineScope nhận vào một khối code suspend, tạo một scope mới, và gọi code trong khối đó bằng coroutine trong scope. Sau đó, nếu kết quả đã sẵn sàng thì nó sẽ được trả về ngay. Nhưng thông thường hàm này sẽ đợi một cách asynchronous (tức là nó nhường lại quyền sử dụng thread trong khi chờ) cho tới khi khối code chạy xong, và trả về kết quả của khối code đó.

Nếu ta tạo coroutine trong coroutineScope, ta sẽ phải đợi tất cả các coroutine đó chạy xong rồi mới đến được dòng code tiếp theo đó. Trong thời gian đợi, các thread của coroutine không bị block và có thể thực hiện các task khác. Nhưng cách này không giúp ta thực hiện fire and forget.

Kết thúc

Trong Kotlin, ta nên tránh sử dụng GlobalScope.launch vì nó không hỗ trợ quản lý vòng đời. Tuy nhiên khi cần thực hiện fire and forget, đây lại là một giải pháp phù hợp. Đó là vì ta cần cho phép xử lý của ta tiếp tục chạy kể cả khi code tạo ra nó đã trả về. Trong thực tế thì phương án sử dụng CoroutineScope.launch là phổ biến hơn. Nhưng về bản chất thì nó không khác gì sử dụng GlobalScope vì ta không dùng tới Job mà hàm launch trả về.

A software developer from Vietnam and is currently living in Japan.

Leave a Reply