Coroutines :: pengalaman praktis

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.1

Apa 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.2

Aliran 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.3

Pustaka 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.5

Untuk 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.6

Tampilan 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.7

Sayangnya, 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.8

Untuk penanganan pengecualian, standar coba tangkap akhirnya bahasa konstruksi tersedia. Sekarang kode yang dapat menampilkan kesalahan di UI mengambil bentuk berikut:


Fig. 2.9

Dalam 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.10

Galat 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.11

Namun, 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.12

Untuk 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.13

Galat 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.14

Mari kita lihat seperti apa tampilan dari pengecualian kita:

  1. Anda perlu mengingat tentang PembatalanException , yang ingin kami abaikan.
  2. Tambahkan log Anda sendiri.
  3. 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.15

Eksekusi 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.16

Bayangkan 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:

  1. Bawa penanganan kesalahan di dalam masing-masing async-corutin.
  2. Jika terjadi kesalahan, batalkan semua coroutine. Untungnya, untuk ini dimungkinkan untuk menentukan pekerjaan orang tua, setelah pembatalan semua anak-anaknya dibatalkan.
  3. 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.17

Pendekatan 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.19

Sekarang 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.20

Sekarang dapatkan dataA menggunakan coroutine bersarang, meninggalkan breakpoint pada logData :


Fig. 2.21

Mencoba 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.22

Contohnya


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.23

Contoh berikut menunjukkan bagaimana menggunakan loop reguler untuk menjalankan kueri berkala dengan interval 5 detik:


Fig. 2.24

Kesimpulan


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:


Source: https://habr.com/ru/post/id442920/


All Articles