Threads vs Coroutines di Kotlin

1. Pengenalan

Dalam tutorial ringkas ini, kita akan membuat dan melaksanakan utas di Kotlin.

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

2. Membuat Benang

Membuat utas di Kotlin sama seperti melakukannya di Java.

Kita boleh melanjutkan kelas Thread (walaupun tidak digalakkan kerana Kotlin tidak menyokong banyak warisan):

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

Atau kita boleh melaksanakan antara muka Runnable :

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

Dan dengan cara yang sama seperti yang kita lakukan di Java, kita dapat melaksanakannya dengan memanggil kaedah start () :

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

Sebagai alternatif, seperti Java 8, Kotlin menyokong Penukaran SAM, oleh itu kita dapat memanfaatkannya dan meneruskan lambda:

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

2.2. Benang Kotlin () Fungsi

Cara lain adalah dengan mempertimbangkan thread fungsi () yang disediakan oleh Kotlin:

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 dan dijalankan hanya dengan:

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

Fungsi ini menerima lima parameter:

  • mula - Untuk menjalankan benang dengan segera
  • isDaemon - Untuk membuat utas sebagai utas daemon
  • konteksClassLoader - Pemuat kelas yang akan digunakan untuk memuat kelas dan sumber
  • nama - Untuk menetapkan nama utas
  • keutamaan - Untuk menetapkan keutamaan utas

3. Kotlin Coroutines

Sangat menggoda untuk berfikir bahawa melahirkan lebih banyak benang dapat membantu kita melaksanakan lebih banyak tugas secara serentak. Malangnya, itu tidak selalu benar.

Membuat terlalu banyak utas sebenarnya boleh menjadikan aplikasi tidak berfungsi dalam beberapa keadaan; benang adalah objek yang mengenakan overhead semasa peruntukan objek dan pengumpulan sampah.

Untuk mengatasi masalah ini, Kotlin memperkenalkan kaedah baru untuk menulis kod tidak sekatan dan tidak segerak; orang Coroutine.

Sama seperti benang, coroutin dapat berjalan secara bersamaan, menunggu, dan berkomunikasi antara satu sama lain dengan perbezaan bahawa membuatnya adalah jauh lebih murah daripada benang.

3.1. Konteks Coroutine

Sebelum membentangkan pembangun coroutine yang disediakan Kotlin di luar kotak, kita harus membincangkan Konteks Coroutine.

Coroutine selalu dilaksanakan dalam beberapa konteks yang merupakan sekumpulan pelbagai elemen.

Unsur utama adalah:

  • Pekerjaan - memodelkan aliran kerja yang dapat dibatalkan dengan pelbagai keadaan dan kitaran hidup yang berakhir dengan penyelesaiannya
  • Dispatcher - menentukan benang atau utas apa yang digunakan coroutine yang sesuai untuk pelaksanaannya. Dengan penghantar, kita dapat membatasi pelaksanaan coroutine ke utas tertentu, mengirimkannya ke kumpulan utas, atau membiarkannya berjalan tanpa batas

Kami akan melihat bagaimana menentukan konteksnya sementara kami menerangkan coroutine pada peringkat seterusnya.

3.2. pelancaran

The pelancaran fungsi adalah pembina coroutine yang memulakan coroutine baru tanpa menyekat thread semasa dan mengembalikan merujuk kepada coroutine sebagai menerusi objek:

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

Ia mempunyai dua parameter pilihan:

  • konteks - Konteks di mana coroutine dijalankan, jika tidak ditentukan, ia mewarisi konteks dari CoroutineScope yang dilancarkan dari
  • start - Pilihan permulaan untuk coroutine. Secara lalai, coroutine dijadualkan untuk dilaksanakan

Perhatikan bahawa kod di atas dijalankan ke kumpulan latar belakang yang dikongsi kerana kami telah menggunakan Dispatchers . Default yang melancarkannya di GlobalScope.

Sebagai alternatif, kita boleh menggunakan GlobalScope.launch yang menggunakan penghantar yang sama:

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

Apabila kita menggunakan Dispatchers.Default atau GlobalScope.launch kita membuat coroutine peringkat teratas. Walaupun ringan, ia masih menggunakan beberapa sumber memori semasa berjalan.

Daripada melancarkan coroutine di GlobalScope, seperti yang biasa kita lakukan dengan benang (benang selalu global), kita dapat melancarkan coroutin dalam ruang lingkup operasi yang kita lakukan:

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

Dalam kes ini, kita memulakan coroutine baru di dalam buildBouting coroutine build (yang akan kita jelaskan kemudian) tanpa menentukan konteksnya. Oleh itu, coroutine akan mewarisi konteks runBlocking .

3.3. tak segerak

Fungsi lain yang diberikan oleh Kotlin untuk membuat coroutine adalah async .

The async fungsi mencipta coroutine baru dan mengembalikan hasil masa depan sebagai contoh tertunda:

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 ujian sehingga kami dapat menghubungkan kod penyekat dengan yang lain yang ditulis dalam gaya penangguhan.

Dengan cara yang sama seperti yang kita lakukan di pembangun coroutine lain, kita dapat menetapkan konteks pelaksanaan:

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

Perhatikan bahawa kita dapat membuat utas baru di mana kita dapat menjalankan coroutine. Walau bagaimanapun, utas khusus adalah sumber yang mahal. Dan, apabila tidak diperlukan lagi, kita harus melepaskannya atau lebih baik menggunakannya lagi di seluruh aplikasi.

4. Kesimpulan

Dalam tutorial ini, kami belajar bagaimana melaksanakan kod asinkron, tanpa sekatan dengan membuat utas.

Sebagai alternatif kepada utas, kami juga telah melihat bagaimana pendekatan Kotlin untuk menggunakan coroutine adalah sederhana dan elegan.

Seperti biasa, semua contoh kod yang ditunjukkan dalam tutorial ini terdapat di Github.