Threads vs Coroutine di Kotlin

1. Perkenalan

Dalam tutorial singkat ini, kita akan membuat dan mengeksekusi thread di Kotlin.

Nanti, kita akan membahas cara menghindarinya sama sekali, demi Kotlin Coroutines.

2. Membuat Thread

Membuat utas di Kotlin mirip dengan melakukannya di Java.

Kita bisa memperluas kelas Thread (meskipun tidak direkomendasikan karena Kotlin tidak mendukung multiple inheritance):

class SimpleThread: Thread() { public override fun run() { println("${Thread.currentThread()} has run.") } }

Atau kita dapat mengimplementasikan antarmuka Runnable :

class SimpleRunnable: Runnable { public override fun run() { println("${Thread.currentThread()} has run.") } }

Dan dengan cara yang sama kita lakukan di Java, kita bisa menjalankannya dengan memanggil metode start () :

val thread = SimpleThread() thread.start() val threadWithRunnable = Thread(SimpleRunnable()) threadWithRunnable.start()

Atau, seperti Java 8, Kotlin mendukung Konversi SAM, oleh karena itu kita dapat memanfaatkannya dan meneruskan lambda:

val thread = Thread { println("${Thread.currentThread()} has run.") } thread.start()

2.2. Fungsi thread Kotlin ()

Cara lain adalah dengan mempertimbangkan function thread () yang Kotlin sediakan:

fun thread( start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, name: String? = null, priority: Int = -1, block: () -> Unit ): Thread

Dengan fungsi ini, utas dapat dibuat instance-nya dan dijalankan hanya dengan:

thread(start = true) { println("${Thread.currentThread()} has run.") }

Fungsi ini menerima lima parameter:

  • start - Untuk segera menjalankan utas
  • isDaemon - Untuk membuat utas sebagai utas daemon
  • contextClassLoader - Pemuat kelas yang akan digunakan untuk memuat kelas dan sumber daya
  • name - Untuk mengatur nama utas
  • prioritas - Untuk mengatur prioritas utas

3. Coroutines Kotlin

Sangat menggoda untuk berpikir bahwa menghasilkan lebih banyak utas dapat membantu kami menjalankan lebih banyak tugas secara bersamaan. Sayangnya, hal itu tidak selalu benar.

Membuat terlalu banyak utas sebenarnya dapat membuat aplikasi berkinerja buruk dalam beberapa situasi; utas adalah objek yang membebankan biaya tambahan selama alokasi objek dan pengumpulan sampah.

Untuk mengatasi masalah ini, Kotlin memperkenalkan cara baru untuk menulis kode asinkron dan non-pemblokiran; Coroutine.

Mirip dengan utas, coroutine dapat berjalan secara bersamaan, menunggu, dan berkomunikasi satu sama lain dengan perbedaan bahwa membuatnya jauh lebih murah daripada utas.

3.1. Konteks Coroutine

Sebelum menyajikan pembangun coroutine yang disediakan Kotlin secara langsung, kita harus membahas Konteks Coroutine.

Coroutine selalu dieksekusi dalam beberapa konteks yang merupakan sekumpulan berbagai elemen.

Elemen utamanya adalah:

  • Pekerjaan - memodelkan alur kerja yang dapat dibatalkan dengan beberapa status dan siklus hidup yang berpuncak pada penyelesaiannya
  • Dispatcher - menentukan utas atau utas apa yang digunakan coroutine terkait untuk pelaksanaannya. Dengan dispatcher, kita dapat membatasi eksekusi coroutine ke thread tertentu, mengirimkannya ke kumpulan thread, atau membiarkannya berjalan tanpa batas

Kita akan melihat bagaimana menentukan konteksnya sementara kita mendeskripsikan coroutine pada tahap selanjutnya.

3.2. meluncurkan

Fungsi peluncuran adalah pembuat coroutine yang memulai coroutine baru tanpa memblokir thread saat ini dan mengembalikan referensi ke coroutine sebagai objek Job :

runBlocking { val job = launch(Dispatchers.Default) { println("${Thread.currentThread()} has run.") } }

Ini memiliki dua parameter opsional:

  • konteks - Konteks di mana coroutine dijalankan, jika tidak ditentukan, itu mewarisi konteks dari CoroutineScope tempat peluncurannya
  • start - Opsi mulai untuk coroutine. Secara default, coroutine segera dijadwalkan untuk dieksekusi

Perhatikan bahwa kode di atas dieksekusi menjadi kumpulan latar belakang utas bersama karena kami telah menggunakan Dispatchers.Default yang meluncurkannya di GlobalScope.

Alternatifnya, kita bisa menggunakan GlobalScope.launch yang menggunakan dispatcher yang sama:

val job = GlobalScope.launch { println("${Thread.currentThread()} has run.") }

Saat kita menggunakan Dispatchers.Default atau GlobalScope.launch kita membuat coroutine tingkat atas. Meskipun ringan, ia masih menghabiskan beberapa sumber daya memori saat dijalankan.

Alih-alih meluncurkan coroutine di GlobalScope, seperti yang biasa kita lakukan dengan utas (utas selalu global), kita dapat meluncurkan coroutine dalam cakupan spesifik operasi yang kita lakukan:

runBlocking { val job = launch { println("${Thread.currentThread()} has run.") } }

Dalam kasus ini, kita memulai coroutine baru di dalam pembuat coroutine runBlocking (yang akan kita jelaskan nanti) tanpa menentukan konteksnya. Jadi, coroutine akan mewarisi konteks runBlocking .

3.3. async

Fungsi lain yang disediakan Kotlin untuk membuat coroutine adalah async .

Fungsi async membuat coroutine baru dan mengembalikan hasil di masa mendatang sebagai instance Deferred:

val deferred = async { [email protected] "${Thread.currentThread()} has run." }

deferred is a non-blocking cancellable future which describes an object that acts as a proxy for a result that is initially unknown.

Like launch, we can specify a context in which to execute the coroutine as well as a start option:

val deferred = async(Dispatchers.Unconfined, CoroutineStart.LAZY) { println("${Thread.currentThread()} has run.") }

In this case, we've launched the coroutine using the Dispatchers.Unconfined which starts coroutines in the caller thread but only until the first suspension point.

Note that Dispatchers.Unconfined is a good fit when a coroutine does not consume CPU time nor updates any shared data.

In addition, Kotlin provides Dispatchers.IO that uses a shared pool of on-demand created threads:

val deferred = async(Dispatchers.IO) { println("${Thread.currentThread()} has run.") }

Dispatchers.IO is recommended when we need to do intensive I/O operations.

3.4. runBlocking

We had an earlier look at runBlocking, but now let's talk about it in more depth.

runBlocking is a function that runs a new coroutine and blocks the current thread until its completion.

By way of example in the previous snippet, we launched the coroutine but we never waited for the result.

In order to wait for the result, we have to call the await() suspend method:

// async code goes here runBlocking { val result = deferred.await() println(result) }

await() is what’s called a suspend function. Suspend functions are only allowed to be called from a coroutine or another suspend function. For this reason, we have enclosed it in a runBlocking invocation.

Kami menggunakan runBlocking dalam fungsi utama dan dalam pengujian sehingga kami dapat menautkan kode pemblokiran ke yang ditulis dalam gaya penangguhan.

Dengan cara yang sama seperti yang kita lakukan pada pembuat coroutine lainnya, kita dapat menyetel konteks eksekusi:

runBlocking(newSingleThreadContext("dedicatedThread")) { val result = deferred.await() println(result) }

Perhatikan bahwa kita dapat membuat utas baru tempat kita dapat menjalankan coroutine. Namun, utas khusus adalah sumber daya yang mahal. Dan, jika tidak diperlukan lagi, kami harus merilisnya atau bahkan menggunakannya kembali di seluruh aplikasi.

4. Kesimpulan

Dalam tutorial ini, kita belajar cara mengeksekusi kode asinkron, non-pemblokiran dengan membuat utas.

Sebagai alternatif dari utas, kita juga telah melihat bagaimana pendekatan Kotlin dalam menggunakan coroutine sederhana dan elegan.

Seperti biasa, semua contoh kode yang ditampilkan dalam tutorial ini tersedia di Github.