Dalam artikel ini saya akan berbicara tentang cara kerja coroutine dan cara membuatnya. Pertimbangkan aplikasi dalam eksekusi paralel berurutan. Mari kita bicara tentang penanganan kesalahan, debugging, dan cara untuk menguji coroutine. Pada akhirnya, saya akan meringkas dan berbicara tentang kesan yang tersisa setelah menerapkan pendekatan ini.
Artikel ini disiapkan berdasarkan bahan laporan saya di
MBLT DEV 2018 , di akhir posting - tautan ke video.
Gaya yang konsisten
Fig. 2.1Apa tujuan para pengembang Corutin? Mereka ingin pemrograman asinkron menjadi sesederhana mungkin. Tidak ada yang lebih mudah daripada mengeksekusi kode "baris demi baris" menggunakan konstruksi sintaksis bahasa: try-catch-akhirnya, loop, pernyataan kondisional, dan sebagainya.
Mari kita pertimbangkan dua fungsi. Masing-masing dieksekusi pada utasnya sendiri (Gbr. 2.1). Yang pertama dieksekusi pada utas
B dan mengembalikan beberapa hasil
dataB , maka kita perlu meneruskan hasil ini ke fungsi kedua, yang mengambil
dataB sebagai argumen dan sudah berjalan pada utas
A. Dengan coroutine, kita dapat menulis kode seperti yang ditunjukkan pada gambar. 2.1. Pertimbangkan cara untuk mencapai ini.
Fungsi
longOpOnB, longOpOnA - yang disebut fungsi
suspend , sebelum
untaiannya dibebaskan, dan setelah menyelesaikan pekerjaannya, ia menjadi sibuk kembali.
Agar kedua fungsi ini benar-benar dilakukan dalam utas berbeda dibandingkan dengan yang disebut, sambil mempertahankan gaya penulisan kode yang βkonsistenβ, kita harus membenamkannya dalam konteks coroutine.
Ini dilakukan dengan membuat coroutine menggunakan apa yang disebut Coroutine Builder. Dalam gambar, ini
diluncurkan , tetapi ada yang lain, misalnya,
async ,
runBlocking . Saya akan membicarakannya nanti.
Argumen terakhir adalah blok kode yang dieksekusi dalam konteks coroutine: memanggil fungsi suspend, yang berarti bahwa semua perilaku di atas hanya mungkin dalam konteks coroutine atau dalam fungsi suspend lainnya.
Ada parameter lain dalam metode Coroutine Builder, misalnya, jenis peluncuran, utas di mana blok akan dieksekusi, dan lainnya.
Manajemen siklus hidup
Coroutine Builder memberi kita nilai balik sebagai nilai balik - subkelas kelas
Pekerjaan (Gbr.2.2). Dengan itu, kita dapat mengatur siklus hidup korutin.
Mulai dengan metode
start () , batalkan dengan metode
cancel () , tunggu sampai pekerjaan selesai menggunakan metode
join ( ), berlangganan ke acara penyelesaian pekerjaan dan banyak lagi.
Fig. 2.2Aliran berubah
Anda dapat mengubah aliran eksekusi coroutine dengan mengubah elemen konteks coroutine yang bertanggung jawab untuk penjadwalan. (Gbr. 2.3)
Misalnya, corutin 1 akan dijalankan di utas
UI , sedangkan corutin 2 di utas diambil dari kumpulan
Dispatchers.IO .
Gbr.2.3Pustaka coroutine juga menyediakan fungsi penangguhan
withContext (CoroutineContext) , yang dengannya Anda dapat beralih di antara utas dalam konteks coroutine. Jadi, melompat di antara utas bisa sangat sederhana:
Fig. 2.4.Kami memulai coroutine kami pada utas UI 1 β tampilkan indikator beban β beralih ke utas kerja 2, membebaskan utas utama β kami melakukan operasi panjang di sana yang tidak dapat dilakukan pada utas UI β mengembalikan hasilnya kembali ke utas UI 3 β dan sudah bekerja di sana dengan itu, menampilkan data yang diterima dan menyembunyikan indikator pemuatan.
Sejauh ini terlihat cukup nyaman, teruskan.
Menunda Fungsi
Pertimbangkan kerja corutin pada contoh kasus yang paling umum - bekerja dengan permintaan jaringan menggunakan pustaka Retrofit 2.
Hal pertama yang perlu kita lakukan adalah mengubah
panggilan balik menjadi fungsi
menangguhkan untuk memanfaatkan fitur coroutine:
Fig. 2.5Untuk mengontrol keadaan coroutine, perpustakaan menyediakan fungsi dari bentuk
suspendXXXXCoroutine , yang menyediakan argumen yang mengimplementasikan antarmuka
Lanjutan , menggunakan metode
resumeWithException dan
melanjutkan yang masing-masing kita dapat melanjutkan coroutine jika terjadi kesalahan dan kesuksesan.
Selanjutnya, kita akan mencari tahu apa yang terjadi ketika metode resumeWithException dipanggil, dan pertama, pastikan bahwa kita perlu membatalkan panggilan permintaan jaringan.
Menunda fungsi. Pembatalan panggilan
Untuk membatalkan panggilan dan tindakan lain yang terkait dengan pelepasan sumber daya yang tidak digunakan, saat menerapkan fungsi penangguhan, Anda dapat menggunakan metode
suspendCancellableCoroutine yang keluar dari kotak (Gbr. 2.6). Di sini, argumen blok sudah mengimplementasikan antarmuka
CancellableContinuation , salah satu metode tambahan di antaranya adalah
invokeOnCancellation , yang memungkinkan Anda mendaftar untuk kesalahan atau acara pembatalan coroutine yang berhasil. Oleh karena itu, di sini juga perlu untuk membatalkan pemanggilan metode.
Fig. 2.6Tampilan perubahan di UI
Sekarang fungsi tunda telah disiapkan untuk permintaan jaringan, Anda dapat menggunakan panggilannya di utas UI coroutine sebagai berurutan, sementara selama pelaksanaan permintaan, aliran akan bebas, dan aliran retrofit akan digunakan untuk menjalankan permintaan.
Dengan demikian, kami menerapkan perilaku asinkron sehubungan dengan aliran UI, tetapi kami menulisnya dengan gaya yang konsisten (Gbr. 2.6).
Jika setelah menerima jawaban Anda perlu melakukan kerja keras, misalnya, menulis data yang diterima ke database, maka fungsi ini, seperti yang telah ditunjukkan, dapat dengan mudah dilakukan menggunakan
withContext pada kumpulan aliran back-stream dan melanjutkan eksekusi pada UI tanpa satu baris kode.
Fig. 2.7Sayangnya, ini tidak semua yang kita butuhkan untuk pengembangan aplikasi. Pertimbangkan penanganan kesalahan.
Menangani kesalahan: coba-tangkap-akhirnya. Batalkan Coroutine: PembatalanException
Pengecualian yang tidak tertangkap di dalam coroutine dianggap tidak tertangani dan dapat menyebabkan aplikasi mogok. Selain situasi normal, pengecualian dilemparkan dengan melanjutkan coroutine menggunakan metode
resumeWithException pada baris yang sesuai dari panggilan ke fungsi menangguhkan. Dalam hal ini, pengecualian yang dilewatkan sebagai argumen dilemparkan tidak berubah. (Gbr. 2.8)
Fig. 2.8Untuk penanganan pengecualian, standar coba tangkap akhirnya bahasa konstruksi tersedia. Sekarang kode yang dapat menampilkan kesalahan di UI mengambil bentuk berikut:
Fig. 2.9Dalam kasus pembatalan coroutine, yang dapat dicapai dengan memanggil metode Job # cancel,
PembatalanException dilemparkan. Pengecualian ini ditangani secara default dan tidak menyebabkan crash atau konsekuensi negatif lainnya.
Namun, ketika menggunakan konstruksi
coba / tangkap , itu akan ditangkap di
blok tangkap , dan Anda harus memperhitungkannya dalam kasus jika Anda ingin menangani hanya situasi yang benar-benar "salah". Misalnya, penanganan kesalahan di UI saat dimungkinkan untuk "membatalkan" permintaan atau kesalahan logging disediakan. Dalam kasus pertama, kesalahan akan ditampilkan kepada pengguna, meskipun sebenarnya tidak ada, dan yang kedua, pengecualian yang tidak berguna akan dicatat dan mengacaukan laporan.
Untuk mengabaikan situasi pembatalan coroutine, Anda perlu sedikit memodifikasi kode:
Fig. 2.10Galat saat masuk
Pertimbangkan pengecualian jejak tumpukan pengecualian.
Jika Anda melempar pengecualian langsung di blok kode coroutine (Gbr. 2.11), maka jejak tumpukan terlihat rapi, dengan hanya beberapa panggilan dari coroutine, ini dengan benar menunjukkan garis dan informasi tentang pengecualian. Dalam hal ini, Anda dapat dengan mudah memahami dari jejak tumpukan di mana tepatnya, di kelas mana dan di mana fungsi pengecualian dilemparkan.
Fig. 2.11Namun, pengecualian yang diteruskan ke metode
resumeWithException dari fungsi
suspend , sebagai aturan, tidak mengandung informasi tentang coroutine di mana itu terjadi. Misalnya (Gbr. 2.12), jika Anda melanjutkan coroutine dari fungsi penangguhan yang diterapkan sebelumnya dengan pengecualian yang sama seperti pada contoh sebelumnya, maka jejak tumpukan tidak akan memberikan informasi tentang di mana harus secara spesifik mencari kesalahan.
Fig. 2.12Untuk memahami coroutine yang dilanjutkan dengan pengecualian, Anda dapat menggunakan
elemen konteks
CoroutineName . (Gbr. 2.13)
Elemen
CoroutineName digunakan untuk debugging, meneruskan nama coroutine ke dalamnya, Anda dapat mengekstraknya dalam fungsi-fungsi yang ditangguhkan dan, misalnya, menambahkan pesan pengecualian. Artinya, setidaknya akan menjadi jelas di mana harus mencari kesalahan.
Pendekatan ini hanya akan berfungsi jika fungsi menangguhkan dikecualikan dari ini:
Fig. 2.13Galat saat masuk. ExceptionHandler
Untuk mengubah logging pengecualian untuk coroutine tertentu, Anda bisa mengatur ExceptionHandler Anda sendiri, yang merupakan salah satu elemen dari konteks coroutine. (Gbr. 2.14)
Pawang harus mengimplementasikan antarmuka
CoroutineExceptionHandler . Menggunakan operator + yang diganti untuk konteks coroutine, Anda dapat mengganti handler pengecualian standar dengan milik Anda. Pengecualian yang tidak tertangani akan jatuh ke dalam metode
handleException , di mana Anda dapat melakukan apa pun yang Anda perlukan dengannya. Misalnya, abaikan sepenuhnya. Ini akan terjadi jika Anda membiarkan pawang kosong atau menambahkan informasi Anda sendiri:
Fig. 2.14Mari kita lihat seperti apa tampilan dari pengecualian kita:
- Anda perlu mengingat tentang PembatalanException , yang ingin kami abaikan.
- Tambahkan log Anda sendiri.
- Ingat tentang perilaku default, yang meliputi pencatatan dan penghentian aplikasi, jika tidak pengecualian akan "menghilang" dan tidak akan jelas apa yang terjadi.
Sekarang, untuk kasus pengecualian, daftar tumpukan jejak akan dikirim ke logcat dengan informasi tambahan:
Fig. 2.15Eksekusi paralel. async
Pertimbangkan operasi paralel fungsi penangguhan.
Async paling cocok untuk mengatur hasil paralel dari berbagai fungsi. Async, seperti
peluncuran - Coroutine Builder. Kemudahannya adalah bahwa, menggunakan metode
await () , ia mengembalikan data jika berhasil atau melempar pengecualian yang telah terjadi selama eksekusi coroutine. Metode menunggu akan menunggu coroutine selesai, jika belum selesai, jika tidak akan segera mengembalikan hasil pekerjaan. Perhatikan bahwa menunggu adalah fungsi penangguhan, dan karena itu tidak dapat dijalankan di luar konteks coroutine atau fungsi penangguhan lainnya.
Menggunakan async, mendapatkan data dari dua fungsi secara paralel akan terlihat seperti ini:
Fig. 2.16Bayangkan kita dihadapkan dengan tugas mendapatkan data dari dua fungsi secara paralel. Kemudian, Anda perlu menggabungkan dan menampilkannya. Jika terjadi kesalahan, Anda perlu menggambar UI, membatalkan semua permintaan saat ini. Kasus seperti ini sering ditemukan dalam praktik.
Dalam hal ini, kesalahan harus ditangani sebagai berikut:
- Bawa penanganan kesalahan di dalam masing-masing async-corutin.
- Jika terjadi kesalahan, batalkan semua coroutine. Untungnya, untuk ini dimungkinkan untuk menentukan pekerjaan orang tua, setelah pembatalan semua anak-anaknya dibatalkan.
- Kami datang dengan implementasi tambahan untuk memahami apakah semua data telah berhasil dimuat. Misalnya, kami menganggap bahwa jika menunggu dikembalikan nol, kesalahan terjadi saat menerima data.
Dengan semua ini dalam pikiran, menerapkan coroutine orangtua menjadi sedikit lebih rumit. Implementasi async-corutin juga rumit:
Fig. 2.17Pendekatan ini bukan satu-satunya yang mungkin. Misalnya, Anda bisa mengimplementasikan eksekusi paralel dengan penanganan kesalahan menggunakan
ExceptionHandler atau
SupervisorJob .
Coroutines bersarang
Mari kita lihat karya coroutine bersarang.
Secara default, coroutine bersarang dibuat menggunakan lingkup eksternal dan mewarisi konteksnya. Akibatnya, coroutine yang bersarang menjadi anak perempuan, dan orang tua eksternal.
Jika kita membatalkan coroutine eksternal, coroutine bersarang yang dibuat dengan cara ini, yang digunakan dalam contoh sebelumnya, juga akan dibatalkan. Ini juga akan berguna ketika meninggalkan layar ketika Anda harus membatalkan permintaan saat ini. Selain itu, orang tua corutin akan selalu menunggu penyelesaian putri.
Anda dapat membuat coroutine yang independen dari eksternal menggunakan lingkup global. Dalam hal ini, ketika coroutine eksternal dibatalkan, yang bersarang akan terus bekerja seolah-olah tidak ada yang terjadi:
Fig. 2.18
Anda dapat membuat anak dari coroutine bersarang global dengan mengganti elemen konteks dengan kunci
Pekerjaan dengan pekerjaan induk, atau Anda dapat sepenuhnya menggunakan konteks coroutine induk. Tetapi dalam hal ini perlu diingat bahwa semua elemen induk coroutine diambil alih: kumpulan thread, pengendali pengecualian, dan sebagainya:
Fig. 2.19Sekarang sudah jelas bahwa jika Anda menggunakan coroutine dari luar, Anda harus memberi mereka kemampuan untuk menginstal instance pekerjaan atau konteks induk. Dan pengembang perpustakaan perlu mempertimbangkan kemungkinan menginstalnya sebagai anak, yang menyebabkan ketidaknyamanan.
Breakpoints
Coroutine mempengaruhi tampilan nilai objek dalam mode debug. Jika Anda meletakkan breakpoint di dalam coroutine berikutnya pada fungsi
logData , maka ketika itu menyala, kita melihat bahwa semuanya baik-baik saja di sini dan nilainya ditampilkan dengan benar:
Fig. 2.20Sekarang dapatkan
dataA menggunakan coroutine bersarang, meninggalkan breakpoint pada
logData :
Fig. 2.21Mencoba memperluas blok ini untuk mencoba menemukan nilai yang diinginkan gagal. Dengan demikian, debugging di hadapan fungsi-menangguhkan menjadi sulit.
Pengujian unit
Pengujian unit cukup mudah. Anda dapat menggunakan Coroutine Builder
runBlocking untuk ini .
runBlocking memblokir utas sampai semua coroutine bersarangnya selesai, yang persis seperti yang Anda butuhkan untuk pengujian.
Sebagai contoh, jika diketahui bahwa suatu tempat di dalam metode coroutine digunakan untuk mengimplementasikannya, maka untuk menguji metode ini Anda hanya perlu membungkusnya di
runBlocking .
runBlocking dapat digunakan untuk menguji fungsi menangguhkan:
Fig. 2.22Contohnya
Akhirnya, saya ingin menunjukkan beberapa contoh penggunaan corutin.
Bayangkan bahwa kita perlu menjalankan tiga pertanyaan A, B, dan C secara paralel, menunjukkan penyelesaiannya dan mencerminkan momen penyelesaian permintaan A dan B.
Untuk melakukan ini, Anda cukup membungkus kueri coroutine A dan B menjadi satu yang umum dan bekerja dengannya sebagai satu kesatuan:
Fig. 2.23Contoh berikut menunjukkan bagaimana menggunakan loop reguler untuk menjalankan kueri berkala dengan interval 5 detik:
Fig. 2.24Kesimpulan
Dari minus, saya perhatikan bahwa coroutine adalah alat yang relatif muda, jadi jika Anda ingin menggunakannya pada prod, Anda harus melakukan ini dengan hati-hati. Ada kesulitan debugging, sebuah boilerplate kecil dalam implementasi hal-hal yang jelas.
Secara umum, coroutine cukup mudah digunakan, terutama untuk mengimplementasikan tugas asinkron yang tidak rumit. Secara khusus, karena fakta bahwa konstruksi bahasa standar dapat digunakan. Coroutine mudah menerima pengujian unit dan semua ini keluar dari kotak dari perusahaan yang sama yang mengembangkan bahasa.
Laporkan video
Ternyata banyak surat. Bagi mereka yang suka mendengarkan lebih banyak - video dari laporan saya di
MBLT DEV 2018 :
Materi yang berguna tentang topik: