Pengantar coroutine

UI responsif merupakan elemen penting untuk aplikasi hebat. Meskipun hal ini mungkin terlihat sepele di aplikasi yang telah Anda build sejauh ini. Saat Anda mulai menambahkan fitur yang lebih canggih, seperti kemampuan jaringan atau database, maka akan semakin sulit untuk menulis kode yang berfungsi dan berperforma baik. Contoh di bawah hanya menggambarkan situasi yang dapat terjadi jika tugas yang berjalan lama, seperti mendownload gambar dari Internet, tidak ditangani dengan benar. Meskipun gambar berfungsi, scrolling terlihat cepat sehingga membuat UI terlihat tidak responsif (dan tidak profesional!).

9f8c54ba29f548cd.gif

Untuk menghindari masalah dengan aplikasi di atas, Anda perlu sedikit mempelajari thread. Thread merupakan konsep yang sedikit abstrak, tetapi Anda dapat menganggapnya sebagai satu jalur eksekusi untuk kode di aplikasi Anda. Setiap baris kode yang Anda tulis adalah petunjuk yang akan dijalankan secara berurutan di thread yang sama.

Anda telah menggunakan thread di Android. Setiap aplikasi Android memiliki thread "utama" default. Thread ini (biasanya) adalah thread UI. Semua kode yang telah Anda tulis hingga sekarang berada di thread utama. Setiap petunjuk (misalnya, satu baris kode) menunggu hingga kode sebelumnya selesai sebelum baris berikutnya dijalankan.

Namun, dalam aplikasi yang berjalan, ada lebih banyak thread selain thread utama. Di balik layar, prosesor tidak benar-benar berfungsi dengan thread terpisah, melainkan beralih bolak-balik di antara rangkaian petunjuk yang berbeda untuk memberikan tampilan multitasking. Thread adalah abstraksi yang dapat Anda gunakan saat menulis kode untuk menentukan jalur eksekusi yang harus dijalankan oleh setiap petunjuk. Dengan thread selain thread utama, aplikasi Anda dapat melakukan tugas yang kompleks, seperti mendownload gambar di latar belakang sementara antarmuka pengguna aplikasi tetap responsif. Ini disebut kode serentak, atau hanya terjadi secara serentak.

Dalam codelab ini, Anda akan mempelajari thread dan cara menggunakan fitur Kotlin yang disebut coroutine untuk menulis kode serentak yang tidak memblokir.

Prasyarat

Yang akan Anda pelajari

  • Pengetian serentak dan alasannya diperlukan
  • Cara menggunakan coroutine dan Thread untuk menulis kode serentak yang tidak memblokir
  • Cara mengakses thread utama untuk menjalankan update UI dengan aman ketika melakukan tugas di latar belakang
  • Cara dan waktu untuk menggunakan berbagai pola serentak (Cakupan/Dispatcher/Ditangguhkan)
  • Cara menulis kode yang berinteraksi dengan resource jaringan

Yang akan Anda buat

  • Dalam codelab ini, Anda akan menulis beberapa program kecil untuk mempelajari cara menggunakan thread dan coroutine di Kotlin

Yang Anda perlukan

  • Komputer dengan browser web modern, seperti Chrome versi terbaru
  • Akses internet di komputer

Multithreading dan serentak

Sejauh ini kami telah memperlakukan aplikasi Android sebagai program dengan satu jalur eksekusi. Anda dapat melakukan banyak hal dengan menjalankan satu eksekusi tersebut, tetapi saat aplikasi semakin berkembang, Anda perlu memikirkan kode serentak.

Dengan serentak, beberapa unit kode dijalankan secara tidak berurutan atau secara paralel sehingga penggunaan resource bisa lebih efisien. Sistem operasi dapat menggunakan karakteristik sistem, bahasa pemrograman, dan unit serentak untuk mengelola multitasking.

fe71122b40bdb5e3.png

Mengapa Anda perlu menggunakan serentak? Seiring aplikasi tersebut berkembang menjadi lebih kompleks, Anda harus membuat kode yang tidak memblokir. Tujuannya agar saat melakukan tugas yang berjalan lama, seperti permintaan jaringan, eksekusi aktivitas lain di aplikasi Anda tidak akan terhenti. Aplikasi bisa tampak tidak responsif bagi pengguna jika Anda tidak mengimplementasikan serentak dengan benar.

Anda akan melihat beberapa contoh yang menunjukkan pemrograman serentak di Kotlin. Semua contoh tersebut dapat berjalan di Kotlin Playground:

https://developer.android.com/training/kotlinplayground

Thread adalah unit kode terkecil yang dapat dijadwalkan dan dijalankan dalam batasan program. Berikut adalah contoh kecil untuk menjalankan kode serentak.

Anda dapat membuat thread sederhana dengan menyediakan lambda. Coba berbagai hal berikut di playground

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

Thread tidak dijalankan hingga fungsi mencapai panggilan fungsi start(). Output akan terlihat seperti ini.

Thread[Thread-0,5,main] has run.

Perhatikan bahwa currentThread() menampilkan instance Thread yang dikonversi ke representasi stringnya dan menampilkan nama, prioritas, dan grup thread dari thread tersebut. Output di atas mungkin akan sedikit berbeda.

Membuat dan menjalankan beberapa thread

Untuk mendemonstrasikan serentak sederhana, mari kita buat beberapa thread untuk dijalankan. Kode akan membuat 3 thread yang mencetak baris informasi dari contoh sebelumnya.

fun main() {
   val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
   repeat(3) {
       Thread {
           println("${Thread.currentThread()} has started")
           for (i in states) {
               println("${Thread.currentThread()} - $i")
               Thread.sleep(50)
           }
       }.start()
   }
}

Output di playground:

Thread[Thread-2,5,main] has started Thread[Thread-2,5,main] - Starting Thread[Thread-0,5,main] - Doing Task 1 Thread[Thread-1,5,main] - Doing Task 1 Thread[Thread-2,5,main] - Doing Task 1 Thread[Thread-0,5,main] - Doing Task 2 Thread[Thread-1,5,main] - Doing Task 2 Thread[Thread-2,5,main] - Doing Task 2 Thread[Thread-0,5,main] - Ending Thread[Thread-2,5,main] - Ending Thread[Thread-1,5,main] - Ending Thread[Thread-0,5,main] has started
Thread[Thread-0,5,main] - Starting
Thread[Thread-1,5,main] has started
Thread[Thread-1,5,main] - Starting

Output di AS(konsol):

Thread[Thread-0,5,main] has started
Thread[Thread-1,5,main] has started
Thread[Thread-2,5,main] has started
Thread[Thread-1,5,main] - Starting
Thread[Thread-0,5,main] - Starting
Thread[Thread-2,5,main] - Starting
Thread[Thread-1,5,main] - Doing Task 1
Thread[Thread-0,5,main] - Doing Task 1
Thread[Thread-2,5,main] - Doing Task 1
Thread[Thread-0,5,main] - Doing Task 2
Thread[Thread-1,5,main] - Doing Task 2
Thread[Thread-2,5,main] - Doing Task 2
Thread[Thread-0,5,main] - Ending
Thread[Thread-2,5,main] - Ending
Thread[Thread-1,5,main] - Ending

Jalankan kode beberapa kali. Anda akan melihat hasil yang bervariasi. Terkadang thread akan tampak berjalan secara berurutan dan di waktu lain konten akan diselingi.

Menggunakan thread adalah cara sederhana untuk mulai menggunakan beberapa tugas dan serentak, tetapi bukan berarti bebas masalah. Sejumlah masalah dapat muncul jika Anda menggunakan Thread langsung dalam kode Anda.

Thread memerlukan banyak resource.

Membuat, beralih, dan mengelola thread memerlukan resource sistem dan waktu yang membatasi jumlah thread mentah yang dapat dikelola secara bersamaan. Biaya pembuatan dapat meningkat secara signifikan.

Meskipun aplikasi yang berjalan akan memiliki beberapa thread, setiap aplikasi akan memiliki satu thread spesial yang secara khusus bertanggung jawab atas UI aplikasi Anda. Thread ini sering disebut sebagai thread utama atau thread UI.

Thread ini bertanggung jawab untuk menjalankan UI aplikasi Anda, dan oleh karenanya, thread utama harus memiliki performa tinggi agar aplikasi dapat berjalan lancar. Setiap tugas yang berjalan lama akan memblokir thread tersebut sampai selesai dan menyebabkan aplikasi Anda tidak responsif.

Sistem operasi melakukan banyak upaya untuk menjaga berbagai hal tetap responsif bagi pengguna. Ponsel saat ini berupaya mengupdate UI sebanyak 60 hingga 120 kali per detik (minimal 60). Waktu yang ada terbatas dan singkat untuk menyiapkan dan menggambar UI (pada 60 frame per detik, setiap update layar akan memerlukan waktu 16 md atau kurang). Android akan melepaskan frame, atau membatalkan upaya penyelesaian satu siklus update untuk mencoba mengejar proses tersebut. Beberapa pelepasan frame dan fluktuasi adalah hal yang normal. Namun, jika terlalu banyak, justru akan membuat aplikasi Anda tidak responsif.

Kondisi race dan perilaku yang tidak dapat diprediksi

Seperti yang telah dibahas, thread merupakan abstraksi cara prosesor menangani beberapa tugas sekaligus. Saat prosesor beralih di antara kumpulan petunjuk di berbagai thread, waktu pasti eksekusi thread dan saat thread dijeda berada di luar kontrol Anda. Anda tidak dapat selalu mengharapkan output yang dapat diprediksi saat menggunakan thread secara langsung.

Misalnya, kode berikut menggunakan pengulangan sederhana untuk menghitung dari 1 sampai 50, tetapi dalam kasus ini, thread baru akan dibuat setiap kenaikan jumlah. Pikirkan seperti apa hasil output yang Anda harapkan, lalu jalankan kode beberapa kali.

fun main() {
   var count = 0
   for (i in 1..50) {
       Thread {
           count += 1
           println("Thread: $i count: $count")
       }.start()
   }
}

Apakah hasilnya sesuai dengan harapan Anda? Apakah hasilnya selalu sama? Berikut contoh output yang kami dapatkan.

Thread: 50 count: 49 Thread: 43 count: 50 Thread: 1 count: 1
Thread: 2 count: 2
Thread: 3 count: 3
Thread: 4 count: 4
Thread: 5 count: 5
Thread: 6 count: 6
Thread: 7 count: 7
Thread: 8 count: 8
Thread: 9 count: 9
Thread: 10 count: 10
Thread: 11 count: 11
Thread: 12 count: 12
Thread: 13 count: 13
Thread: 14 count: 14
Thread: 15 count: 15
Thread: 16 count: 16
Thread: 17 count: 17
Thread: 18 count: 18
Thread: 19 count: 19
Thread: 20 count: 20
Thread: 21 count: 21
Thread: 23 count: 22
Thread: 22 count: 23
Thread: 24 count: 24
Thread: 25 count: 25
Thread: 26 count: 26
Thread: 27 count: 27
Thread: 30 count: 28
Thread: 28 count: 29
Thread: 29 count: 41
Thread: 40 count: 41
Thread: 39 count: 41
Thread: 41 count: 41
Thread: 38 count: 41
Thread: 37 count: 41
Thread: 35 count: 41
Thread: 33 count: 41
Thread: 36 count: 41
Thread: 34 count: 41
Thread: 31 count: 41
Thread: 32 count: 41
Thread: 44 count: 42
Thread: 46 count: 43
Thread: 45 count: 44
Thread: 47 count: 45
Thread: 48 count: 46
Thread: 42 count: 47
Thread: 49 count: 48

Berbeda dengan yang diucapkan kode, sepertinya thread terakhir dieksekusi terlebih dahulu, dan beberapa thread lainnya dieksekusi secara acak. Jika melihat "jumlah" untuk beberapa iterasi, Anda akan melihat bahwa iterasi tetap tidak berubah setelah beberapa thread. Lebih anehnya lagi, jumlah tersebut mencapai angka 50 di Thread 43 meskipun output menunjukkan bahwa hanya thread kedua yang akan dijalankan. Dilihat dari outputnya saja, tidak mungkin untuk mengetahui berapa nilai akhir count.

Ini hanyalah salah satu cara thread bisa menyebabkan perilaku yang tidak dapat diprediksi. Saat menggunakan beberapa thread, Anda juga dapat mengalami kondisi yang disebut dengan kondisi race. Kondisi demikian terjadi ketika beberapa thread mencoba mengakses nilai yang sama dalam memori di waktu bersamaan. Kondisi race dapat menyebabkan kesulitan mereproduksi dan melihat bug secara acak sehingga dapat menyebabkan aplikasi Anda error, bahkan sering kali tidak dapat diprediksi.

Masalah performa, kondisi race, dan kesulitan mereproduksi bug adalah beberapa alasan mengapa kami tidak menyarankan Anda untuk menggunakan thread secara langsung. Sebagai gantinya, Anda akan mempelajari fitur di Kotlin yang disebut Coroutine yang akan membantu Anda menulis kode serentak.

Android menyediakan tempat untuk membuat dan menggunakan thread untuk tugas latar belakang secara langsung, tetapi Kotlin juga menawarkan Coroutine yang menawarkan cara yang lebih fleksibel dan mudah untuk mengelola serentak.

Coroutine memungkinkan multitasking, tetapi juga memberikan tingkat abstraksi lain yang lebih dari sekadar menggunakan thread. Salah satu fitur utama coroutine adalah kemampuan untuk menyimpan status sehingga bisa dihentikan dan dilanjutkan. Coroutine mungkin dapat dijalankan atau mungkin juga tidak.

Status, yang diwakili oleh lanjutan, memungkinkan bagian kode memberi sinyal saat harus menyerahkan kontrol atau menunggu coroutine lain menyelesaikan tugasnya sebelum melanjutkan. Alur ini disebut multitasking sederhana. Implementasi coroutine oleh Kotlin telah menambahkan sejumlah fitur untuk membantu multitasking. Selain meneruskan, pembuatan coroutine juga mencakup pekerjaan di Job, unit kerja yang dapat dibatalkan dengan siklus proses di dalam CoroutineScope. CoroutineScope adalah konteks yang memberlakukan pembatalan dan aturan lain bagi turunannya dan turunan mereka secara rekursif. Dispatcher mengelola thread yang akan digunakan coroutine untuk eksekusinya dengan meniadakan tanggung jawab waktu dan tempat untuk menggunakan thread baru dari developer.

Tugas

Unit tugas yang dapat dibatalkan, seperti yang dibuat dengan fungsi launch().

CoroutineScope

Fungsi yang digunakan untuk membuat coroutine baru seperti launch() dan async() memperluas CoroutineScope.

Dispatcher

Menentukan thread yang akan digunakan oleh coroutine. Dispatcher Main akan selalu menjalankan coroutine di thread utama, sementara petugas operator seperti Default, IO, atau Unconfined akan menggunakan thread lain.

Anda akan mempelajari hal ini lebih lanjut nanti, tetapi Dispatchers adalah salah satu cara yang membuat coroutine dapat berperforma baik. Seseorang menghindari biaya performa saat menginisialisasi thread baru.

Mari sesuaikan contoh sebelumnya untuk menggunakan coroutine.

import kotlinx.coroutines.*

fun main() {
    repeat(3) {
        GlobalScope.launch {
            println("Hi from ${Thread.currentThread()}")
        }
    }
}
Hi from Thread[DefaultDispatcher-worker-2@coroutine#2,5,main]
Hi from Thread[DefaultDispatcher-worker-1@coroutine#1,5,main]
Hi from Thread[DefaultDispatcher-worker-1@coroutine#3`,5,main]

Cuplikan di atas membuat tiga coroutine di Cakupan Global menggunakan Dispatcher default. GlobalScope memungkinkan coroutine di dalamnya untuk berjalan selama aplikasi berjalan. Karena alasan yang kita bahas terkait thread utama, coroutine ini tidak disarankan di luar kode contoh. Saat Anda menggunakan coroutine dalam aplikasi, kami akan menggunakan cakupan lain.

Fungsi launch() akan membuat coroutine dari kode yang disertakan dalam objek Tugas yang dapat dibatalkan. launch() digunakan jika nilai pengembalian tidak diperlukan di luar batasan coroutine

Mari lihat tanda tangan lengkap dari launch() untuk memahami konsep penting berikutnya dalam coroutine.

fun CoroutineScope.launch {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
}

Di balik layar, blok kode yang Anda berikan untuk diluncurkan telah ditandai dengan kata kunci suspend. Menangguhkan sinyal bahwa blok kode atau fungsi dapat dijeda atau dilanjutkan.

Penjelasan singkat runBlocking

Contoh berikutnya akan menggunakan runBlocking() yang sesuai dengan namanya, memulai coroutine baru dan memblokir thread saat ini hingga selesai. Kode ini terutama digunakan untuk menjembatani kode yang memblokir dan tidak memblokir dalam fungsi dan pengujian utama. Anda tidak akan sering menggunakannya dalam kode Android biasa.

import kotlinx.coroutines.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

val formatter = DateTimeFormatter.ISO_LOCAL_TIME
val time = { formatter.format(LocalDateTime.now()) }

suspend fun getValue(): Double {
    println("entering getValue() at ${time()}")
    delay(3000)
    println("leaving getValue() at ${time()}")
    return Math.random()
}

fun main() {
    runBlocking {
        val num1 = getValue()
        val num2 = getValue()
        println("result of num1 + num2 is ${num1 + num2}")
    }
}

getValue() menampilkan angka acak setelah waktu penundaan tertentu. Ini menggunakan DateTimeFormatter. Untuk menggambarkan waktu masuk dan keluar yang sesuai. Fungsi utama memanggil getValue() sebanyak dua kali dan menampilkan jumlah.

entering getValue() at 17:44:52.311
leaving getValue() at 17:44:55.319
entering getValue() at 17:44:55.32
leaving getValue() at 17:44:58.32
result of num1 + num2 is 1.4320332550421415

Untuk melihat cara kerjanya, ganti fungsi main() (menyimpan semua kode lainnya) dengan fungsi berikut ini.

fun main() {
    runBlocking {
        val num1 = async { getValue() }
        val num2 = async { getValue() }
        println("result of num1 + num2 is ${num1.await() + num2.await()}")
    }
}

Kedua panggilan ke getValue() bersifat independen dan tidak perlu coroutine untuk dimenangguhkannya. Kotlin memiliki fungsi asinkron yang mirip dengan peluncuran. Fungsi async() ditentukan sebagai berikut.

Fun CoroutineScope.async() {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
}: Deferred<T>

Fungsi async() menampilkan nilai jenis Deferred. Deferred adalah Job yang dapat dibatalkan dan dapat menyimpan referensi ke nilai selanjutnya. Dengan menggunakan Deferred, Anda tetap dapat memanggil fungsi seolah-olah fungsi tersebut langsung menampilkan nilai - Deferred hanya berfungsi sebagai placeholder karena Anda tidak dapat memastikan kapan tugas asinkron akan ditampilkan. Deferred (juga disebut Promise atau Future dalam bahasa lain) menjamin bahwa nilai akan dikembalikan ke objek ini di lain waktu. Di sisi lain, tugas asinkron tidak akan memblokir atau menunggu eksekusi secara default. Untuk memulai baris kode saat ini yang harus menunggu output Deferred, Anda dapat memanggil await(). Kode ini akan menampilkan nilai mentah.

entering getValue() at 22:52:25.025
entering getValue() at 22:52:25.03
leaving getValue() at 22:52:28.03
leaving getValue() at 22:52:28.032
result of num1 + num2 is 0.8416379026501276

Waktu untuk menandai fungsi sebagai ditangguhkan

Pada contoh sebelumnya, Anda mungkin telah melihat bahwa fungsi getValue() juga ditentukan dengan kata kunci suspend. Alasannya karena fungsi tersebut memanggil delay() yang juga merupakan fungsi suspend. Setiap kali fungsi memanggil fungsi suspend lain, fungsi tersebut juga harus merupakan fungsi suspend.

Jika demikian, mengapa fungsi main() yang tidak bisa digunakan dalam contoh kita ditandai dengan suspend? Fungsi tersebut akan memanggil getValue(), setelahnya.

Belum tentu. getValue() sebenarnya dipanggil dalam fungsi yang diteruskan ke runBlocking() yang merupakan fungsi suspend, yang mirip dengan fungsi yang diteruskan ke launch() dan async(). Namun, getValue() tidak dipanggil dalam main(), atau runBlocking() bukan merupakan fungsi suspend, sehingga main() tidak ditandai dengan suspend. Jika suatu fungsi tidak memanggil fungsi suspend, fungsi tersebut tidak harus berupa fungsi suspend itu sendiri.

Di awal codelab ini, Anda telah melihat contoh berikut yang menggunakan beberapa thread. Dengan pengetahuan Anda tentang coroutine, coba tuliskan ulang kode untuk menggunakan coroutine, bukan Thread.

Catatan: Anda tidak perlu mengedit pernyataan println() meskipun mengacu pada Thread.

fun main() {
   val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
   repeat(3) {
       Thread {
           println("${Thread.currentThread()} has started")
           for (i in states) {
               println("${Thread.currentThread()} - $i")
               Thread.sleep(50)
           }
       }.start()
   }
}
import kotlinx.coroutines.*

fun main() {
   val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
   repeat(3) {
       GlobalScope.launch {
           println("${Thread.currentThread()} has started")
           for (i in states) {
               println("${Thread.currentThread()} - $i")
               delay(5000)
           }
       }
   }
}

Anda telah mempelajari

  • Alasan serentak diperlukan
  • Pengertian thread, dan alasan pentingnya thread untuk serentak
  • Cara menulis kode serentak di Kotlin menggunakan coroutine
  • Waktu untuk menandai fungsi sebagai "menangguhkan"
  • Peran CoroutineScope, Tugas, dan Dispatcher
  • Perbedaan antara Ditangguhkan dan Menunggu