API non-blocking meminta pekerjaan dilakukan, lalu mengembalikan kontrol ke thread panggilan sehingga thread tersebut dapat melakukan pekerjaan lain sebelum penyelesaian operasi yang diminta. API ini berguna untuk kasus saat pekerjaan yang diminta mungkin sedang berlangsung atau mungkin memerlukan penantian penyelesaian I/O atau IPC, ketersediaan resource sistem yang sangat diperebutkan, atau input pengguna sebelum pekerjaan dapat dilanjutkan. API yang didesain dengan baik secara khusus menyediakan cara untuk membatalkan operasi yang sedang berlangsung dan menghentikan pekerjaan yang dilakukan atas nama pemanggil asli, sehingga menjaga kesehatan sistem dan daya tahan baterai saat operasi tidak lagi diperlukan.
API asinkron adalah salah satu cara untuk mencapai perilaku non-blocking. API Asinkron menerima beberapa bentuk kelanjutan atau callback yang diberi tahu saat operasi selesai, atau peristiwa lain selama progres operasi.
Ada dua motivasi utama untuk menulis API asinkron:
- Menjalankan beberapa operasi secara bersamaan, dengan operasi ke-N harus dimulai sebelum operasi ke-N-1 selesai.
- Menghindari pemblokiran thread panggilan hingga operasi selesai.
Kotlin sangat menganjurkan konkurensi terstruktur, serangkaian prinsip dan API yang dibangun di atas fungsi penangguhan yang memisahkan eksekusi kode sinkron dan asinkron dari perilaku pemblokiran thread. Fungsi penangguhan bersifat non-blocking dan sinkron.
Fungsi penangguhan:
- Jangan memblokir thread panggilan mereka, tetapi serahkan thread eksekusi mereka sebagai detail implementasi sambil menunggu hasil operasi yang dijalankan di tempat lain.
- Dieksekusi secara sinkron dan tidak mengharuskan pemanggil API non-pemblokiran untuk terus dieksekusi secara bersamaan dengan tugas non-pemblokiran yang dimulai oleh panggilan API.
Halaman ini menjelaskan dasar ekspektasi minimum yang dapat dipegang developer dengan aman saat menggunakan API asinkron dan non-blocking, diikuti dengan serangkaian resep untuk membuat API yang memenuhi ekspektasi ini dalam bahasa Kotlin atau Java, di platform Android atau library Jetpack. Jika ragu, pertimbangkan ekspektasi developer sebagai persyaratan untuk setiap permukaan API baru.
Ekspektasi developer untuk API asinkron
Ekspektasi berikut ditulis dari sudut pandang API yang tidak menangguhkan, kecuali dinyatakan lain.
API yang menerima callback biasanya bersifat asinkron
Jika API menerima callback yang tidak didokumentasikan hanya untuk dipanggil di tempat, (yaitu, hanya dipanggil oleh thread pemanggil sebelum panggilan API itu sendiri ditampilkan,) API dianggap asinkron dan API tersebut harus memenuhi semua ekspektasi lain yang didokumentasikan di bagian berikut.
Contoh callback yang hanya dipanggil di tempat adalah fungsi peta atau filter tingkat tinggi yang memanggil pemeta atau predikat pada setiap item dalam koleksi sebelum ditampilkan.
API Asinkron harus ditampilkan secepat mungkin
Developer mengharapkan API asinkron bersifat non-blocking dan ditampilkan dengan cepat setelah memulai permintaan untuk operasi. API asinkron harus selalu aman untuk dipanggil kapan saja, dan memanggil API asinkron tidak boleh menyebabkan frame yang tidak lancar atau ANR.
Banyak operasi dan sinyal siklus proses dapat dipicu oleh platform atau library sesuai permintaan, dan mengharapkan developer memiliki pengetahuan global tentang semua potensi situs panggilan untuk kode mereka tidak dapat dipertahankan. Misalnya, Fragment
dapat ditambahkan ke FragmentManager
dalam transaksi sinkron sebagai respons
terhadap pengukuran dan tata letak View
saat konten aplikasi harus diisi untuk mengisi
ruang yang tersedia (seperti RecyclerView
). LifecycleObserver
yang merespons
callback siklus proses onStart
fragmen ini dapat secara wajar melakukan operasi
startup satu kali di sini, dan ini mungkin berada di jalur kode penting untuk menghasilkan
frame animasi tanpa jank. Developer harus selalu merasa yakin bahwa
memanggil API asinkron apa pun sebagai respons terhadap jenis callback siklus proses ini
tidak akan menyebabkan frame yang tersendat.
Hal ini menyiratkan bahwa pekerjaan yang dilakukan oleh API asinkron sebelum ditampilkan harus sangat ringan; membuat catatan permintaan dan callback terkait serta mendaftarkannya dengan mesin eksekusi yang melakukan pekerjaan paling banyak. Jika mendaftar untuk operasi asinkron memerlukan IPC, penerapan API harus melakukan tindakan apa pun yang diperlukan untuk memenuhi ekspektasi developer ini. Hal ini dapat mencakup satu atau beberapa hal berikut:
- Menerapkan IPC pokok sebagai panggilan binder satu arah
- Melakukan panggilan binder dua arah ke server sistem yang penyelesaian pendaftarannya tidak memerlukan pengambilan kunci yang sangat diperebutkan
- Memposting permintaan ke thread pekerja dalam proses aplikasi untuk melakukan pendaftaran pemblokiran melalui IPC
API asinkron harus menampilkan void dan hanya memunculkan pengecualian untuk argumen yang tidak valid
Async API harus melaporkan semua hasil operasi yang diminta ke callback yang diberikan. Hal ini memungkinkan developer menerapkan jalur kode tunggal untuk penanganan keberhasilan dan error.
API asinkron dapat memeriksa argumen untuk nilai null dan memunculkan NullPointerException
, atau
memeriksa apakah argumen yang diberikan berada dalam rentang yang valid dan memunculkan
IllegalArgumentException
. Misalnya, untuk fungsi yang menerima float
dalam rentang 0
hingga 1f
, fungsi dapat memeriksa apakah parameter berada dalam
rentang ini dan menampilkan IllegalArgumentException
jika berada di luar rentang, atau
String
singkat dapat diperiksa kesesuaiannya dengan format yang valid seperti
khusus alfanumerik. (Ingatlah bahwa server sistem tidak boleh memercayai proses aplikasi! Setiap layanan sistem harus menduplikasi pemeriksaan ini di layanan sistem itu sendiri.)
Semua error lainnya harus dilaporkan ke callback yang diberikan. Hal ini mencakup, tetapi tidak terbatas pada:
- Kegagalan terminal operasi yang diminta
- Pengecualian keamanan untuk otorisasi atau izin yang tidak ada yang diperlukan untuk menyelesaikan operasi
- Melebihi kuota untuk melakukan operasi
- Proses aplikasi tidak cukup "latar depan" untuk melakukan operasi
- Hardware yang diperlukan telah terputus
- Kegagalan jaringan
- Waktu tunggu
- Proses jarak jauh tidak tersedia atau dihentikan Binder
API asinkron harus menyediakan mekanisme pembatalan
API asinkron harus menyediakan cara untuk menunjukkan kepada operasi yang sedang berjalan bahwa pemanggil tidak lagi peduli dengan hasilnya. Operasi pembatalan ini harus menandakan dua hal:
Referensi tetap ke callback yang diberikan oleh pemanggil harus dilepaskan
Callback yang diberikan ke API asinkron dapat berisi referensi tetap ke grafik objek besar, dan pekerjaan yang sedang berlangsung yang menyimpan referensi tetap ke callback tersebut dapat mencegah pengumpulan sampah grafik objek tersebut. Dengan melepaskan referensi callback ini saat pembatalan, grafik objek ini dapat memenuhi syarat untuk pengumpulan sampah lebih cepat daripada jika tugas diizinkan berjalan hingga selesai.
Mesin eksekusi yang melakukan pekerjaan untuk pemanggil dapat menghentikan pekerjaan tersebut
Tugas yang dimulai oleh panggilan API asinkron dapat menimbulkan biaya yang tinggi dalam penggunaan daya atau resource sistem lainnya. API yang memungkinkan pemanggil memberikan sinyal saat pekerjaan ini tidak diperlukan lagi mengizinkan penghentian pekerjaan tersebut sebelum dapat menggunakan lebih banyak resource sistem.
Pertimbangan khusus untuk aplikasi yang di-cache atau terhenti
Saat mendesain API asinkron tempat callback berasal dari proses sistem dan dikirimkan ke aplikasi, pertimbangkan hal berikut:
- Proses dan siklus proses aplikasi: proses aplikasi penerima mungkin dalam status di-cache.
- Penghentian aplikasi yang di-cache: proses aplikasi penerima mungkin dihentikan.
Saat proses aplikasi memasuki status cache, berarti proses tersebut tidak secara aktif menghosting komponen yang terlihat oleh pengguna seperti aktivitas dan layanan. Aplikasi tetap berada di memori jika aplikasi tersebut terlihat oleh pengguna lagi, tetapi sementara itu tidak boleh melakukan pekerjaan. Dalam sebagian besar kasus, Anda harus menjeda pengiriman callback aplikasi saat aplikasi tersebut memasuki status dalam cache dan melanjutkan saat aplikasi keluar dari status dalam cache, agar tidak memicu pekerjaan dalam proses aplikasi dalam cache.
Aplikasi yang di-cache juga dapat dibekukan. Saat dibekukan, aplikasi menerima waktu CPU nol dan tidak dapat melakukan pekerjaan apa pun. Semua panggilan ke callback terdaftar aplikasi tersebut akan di-buffer dan dikirimkan saat aplikasi tidak dibekukan.
Transaksi yang di-buffer ke callback aplikasi mungkin sudah tidak berlaku saat aplikasi dibuka dan memprosesnya. Buffer bersifat terbatas, dan jika meluap akan menyebabkan aplikasi penerima error. Untuk menghindari aplikasi yang kewalahan dengan peristiwa yang sudah tidak berlaku atau meluapnya buffer, jangan kirim callback aplikasi saat prosesnya dibekukan.
Sedang ditinjau:
- Anda harus mempertimbangkan untuk menjeda pengiriman callback aplikasi saat proses aplikasi di-cache.
- Anda HARUS menjeda pengiriman callback aplikasi saat proses aplikasi dibekukan.
Pelacakan status
Untuk melacak kapan aplikasi memasuki atau keluar dari status yang di-cache:
mActivityManager.addOnUidImportanceListener(
new UidImportanceListener() { ... },
IMPORTANCE_CACHED);
Untuk melacak kapan aplikasi dibekukan atau tidak dibekukan:
IBinder binder = <...>;
binder.addFrozenStateChangeCallback(executor, callback);
Strategi untuk melanjutkan pengiriman callback aplikasi
Baik Anda menjeda pengiriman callback aplikasi saat aplikasi memasuki status cache atau status dibekukan, saat aplikasi keluar dari status tersebut, Anda harus melanjutkan pengiriman callback terdaftar aplikasi setelah aplikasi keluar dari status tersebut hingga aplikasi membatalkan pendaftaran callback-nya atau proses aplikasi berakhir.
Contoh:
IBinder binder = <...>;
bool shouldSendCallbacks = true;
binder.addFrozenStateChangeCallback(executor, (who, state) -> {
if (state == IBinder.FrozenStateChangeCallback.STATE_FROZEN) {
shouldSendCallbacks = false;
} else if (state == IBinder.FrozenStateChangeCallback.STATE_UNFROZEN) {
shouldSendCallbacks = true;
}
});
Atau, Anda dapat menggunakan RemoteCallbackList
yang menangani agar tidak
mengirimkan callback ke proses target saat dibekukan.
Contoh:
RemoteCallbackList<IInterface> rc =
new RemoteCallbackList.Builder<IInterface>(
RemoteCallbackList.FROZEN_CALLEE_POLICY_DROP)
.setExecutor(executor)
.build();
rc.register(callback);
rc.broadcast((callback) -> callback.foo(bar));
callback.foo()
dipanggil hanya jika proses tidak dibekukan.
Aplikasi sering menyimpan update yang diterima menggunakan callback sebagai snapshot status terbaru. Pertimbangkan API hipotetis agar aplikasi dapat memantau persentase baterai yang tersisa:
interface BatteryListener {
void onBatteryPercentageChanged(int newPercentage);
}
Pertimbangkan skenario saat beberapa peristiwa perubahan status terjadi saat aplikasi dibekukan. Saat aplikasi dibuka, Anda hanya boleh mengirimkan status terbaru ke aplikasi dan membatalkan perubahan status yang sudah tidak berlaku. Pengiriman ini harus terjadi segera saat aplikasi dibuka agar aplikasi dapat "mengejar". Hal ini dapat dicapai sebagai berikut:
RemoteCallbackList<IInterface> rc =
new RemoteCallbackList.Builder<IInterface>(
RemoteCallbackList.FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT)
.setExecutor(executor)
.build();
rc.register(callback);
rc.broadcast((callback) -> callback.onBatteryPercentageChanged(value));
Dalam beberapa kasus, Anda dapat melacak nilai terakhir yang dikirimkan ke aplikasi sehingga aplikasi tidak perlu diberi tahu tentang nilai yang sama setelah dibatalkan pembekuannya.
Status dapat dinyatakan sebagai data yang lebih kompleks. Pertimbangkan API hipotetis agar aplikasi diberi tahu tentang antarmuka jaringan:
interface NetworkListener {
void onAvailable(Network network);
void onLost(Network network);
void onChanged(Network network);
}
Saat menjeda notifikasi ke aplikasi, Anda harus mengingat kumpulan jaringan dan status yang terakhir dilihat aplikasi. Setelah dilanjutkan, sebaiknya beri tahu aplikasi tentang jaringan lama yang hilang, jaringan baru yang tersedia, dan jaringan yang sudah ada yang statusnya telah berubah - dalam urutan ini.
Jangan memberi tahu aplikasi tentang jaringan yang tersedia lalu terputus saat callback dijeda. Aplikasi tidak boleh menerima akun lengkap peristiwa yang terjadi saat aplikasi dibekukan, dan dokumentasi API tidak boleh berjanji untuk mengirimkan aliran peristiwa tanpa terputus di luar status siklus proses eksplisit. Dalam contoh ini, jika aplikasi perlu memantau ketersediaan jaringan secara terus-menerus, maka aplikasi harus tetap dalam status siklus proses yang mencegahnya di-cache atau dibekukan.
Dalam peninjauan, Anda harus menggabungkan peristiwa yang terjadi setelah menjeda dan sebelum melanjutkan notifikasi, serta mengirimkan status terbaru ke callback aplikasi terdaftar secara ringkas.
Pertimbangan untuk dokumentasi developer
Penayangan peristiwa asinkron mungkin tertunda, baik karena pengirim menjeda penayangan selama jangka waktu tertentu seperti yang ditunjukkan di bagian sebelumnya atau karena aplikasi penerima tidak menerima cukup resource perangkat untuk memproses peristiwa secara tepat waktu.
Mencegah developer membuat asumsi tentang waktu antara saat aplikasi mereka diberi tahu tentang suatu peristiwa dan waktu terjadinya peristiwa tersebut.
Ekspektasi developer untuk menangguhkan API
Developer yang memahami konkurensi terstruktur Kotlin mengharapkan perilaku berikut dari API yang menangguhkan:
Fungsi penangguhan harus menyelesaikan semua pekerjaan terkait sebelum kembali atau memunculkan pengecualian
Hasil operasi non-blocking ditampilkan sebagai nilai hasil fungsi normal, dan error dilaporkan dengan memunculkan pengecualian. (Hal ini sering kali berarti bahwa parameter callback tidak diperlukan.)
Fungsi penangguhan hanya boleh memanggil parameter callback di tempat
Fungsi penangguhan harus selalu menyelesaikan semua tugas terkait sebelum kembali, sehingga fungsi tersebut tidak boleh memanggil callback yang diberikan atau parameter fungsi lainnya atau mempertahankan referensi ke callback atau parameter fungsi tersebut setelah fungsi penangguhan kembali.
Fungsi penangguhan yang menerima parameter callback harus mempertahankan konteks kecuali didokumentasikan lain
Memanggil fungsi dalam fungsi penangguhan akan menyebabkannya berjalan di
CoroutineContext
pemanggil. Karena fungsi penangguhan harus menyelesaikan semua
tugas terkait sebelum menampilkan atau memunculkan pengecualian, dan hanya boleh memanggil parameter
callback di tempat, ekspektasi defaultnya adalah bahwa callback tersebut juga dijalankan di CoroutineContext
panggilan menggunakan dispatcher terkaitnya. Jika tujuan API adalah menjalankan callback di luar CoroutineContext
panggilan, perilaku ini harus didokumentasikan dengan jelas.
Fungsi penangguhan harus mendukung pembatalan tugas kotlinx.coroutines
Setiap fungsi penangguhan yang ditawarkan harus bekerja sama dengan pembatalan tugas seperti yang ditentukan
oleh kotlinx.coroutines
. Jika tugas panggilan operasi yang sedang berlangsung dibatalkan, fungsi harus dilanjutkan dengan CancellationException
sesegera mungkin agar pemanggil dapat membersihkan dan melanjutkan sesegera mungkin. Hal ini ditangani secara otomatis oleh suspendCancellableCoroutine
dan API penangguhan lainnya yang ditawarkan oleh kotlinx.coroutines
. Penerapan library umumnya tidak boleh menggunakan suspendCoroutine
secara langsung, karena tidak mendukung perilaku pembatalan ini secara default.
Fungsi penangguhan yang melakukan pekerjaan pemblokiran di latar belakang (thread non-utama atau UI) harus menyediakan cara untuk mengonfigurasi dispatcher yang digunakan
Tidak direkomendasikan untuk membuat fungsi pemblokiran ditangguhkan sepenuhnya untuk beralih thread.
Memanggil fungsi penangguhan tidak boleh menyebabkan pembuatan thread tambahan tanpa mengizinkan developer menyediakan thread atau kumpulan thread mereka sendiri untuk melakukan pekerjaan tersebut. Misalnya, konstruktor dapat menerima
CoroutineContext
yang digunakan untuk melakukan tugas latar belakang untuk
metode class.
Fungsi penangguhan yang akan menerima parameter CoroutineContext
atau
Dispatcher
opsional hanya untuk beralih ke dispatcher tersebut guna melakukan
pekerjaan pemblokiran, sebaiknya mengekspos fungsi pemblokiran yang mendasarinya dan merekomendasikan agar
developer yang memanggil menggunakan panggilan mereka sendiri ke withContext untuk mengarahkan pekerjaan ke
dispatcher yang dipilih.
Kelas yang meluncurkan coroutine
Class yang meluncurkan coroutine harus memiliki CoroutineScope
untuk melakukan
operasi peluncuran tersebut. Mematuhi prinsip konkurensi terstruktur menyiratkan pola struktural berikut untuk mendapatkan dan mengelola cakupan tersebut.
Sebelum menulis class yang meluncurkan tugas serentak ke cakupan lain, pertimbangkan pola alternatif:
class MyClass {
private val requests = Channel<MyRequest>(Channel.UNLIMITED)
suspend fun handleRequests() {
coroutineScope {
for (request in requests) {
// Allow requests to be processed concurrently;
// alternatively, omit the [launch] and outer [coroutineScope]
// to process requests serially
launch {
processRequest(request)
}
}
}
}
fun submitRequest(request: MyRequest) {
requests.trySend(request).getOrThrow()
}
}
Mengekspos suspend fun
untuk melakukan pekerjaan serentak memungkinkan pemanggil memanggil
operasi dalam konteksnya sendiri, sehingga tidak perlu MyClass
mengelola
CoroutineScope
. Serialisasi pemrosesan permintaan menjadi lebih sederhana dan status sering kali dapat ada sebagai variabel lokal handleRequests
, bukan sebagai properti class yang memerlukan sinkronisasi tambahan.
Class yang mengelola coroutine harus mengekspos metode tutup dan batalkan
Class yang meluncurkan coroutine sebagai detail implementasi harus menawarkan cara untuk
menghentikan tugas serentak yang sedang berlangsung tersebut secara bersih agar tidak membocorkan
tugas serentak yang tidak terkontrol ke cakupan induk. Biasanya ini berbentuk
membuat Job
turunan dari CoroutineContext
yang disediakan:
private val myJob = Job(parent = `CoroutineContext`[Job])
private val myScope = CoroutineScope(`CoroutineContext` + myJob)
fun cancel() {
myJob.cancel()
}
Metode join()
juga dapat disediakan untuk memungkinkan kode pengguna menunggu
penyelesaian tugas serentak yang sedang dilakukan oleh objek.
(Ini dapat mencakup pekerjaan pembersihan yang dilakukan dengan membatalkan operasi.)
suspend fun join() {
myJob.join()
}
Penamaan operasi terminal
Nama yang digunakan untuk metode yang menutup tugas serentak yang dimiliki oleh objek yang masih dalam proses secara bersih harus mencerminkan kontrak perilaku tentang cara penutupan terjadi:
Gunakan close()
saat operasi yang sedang berlangsung dapat selesai, tetapi tidak ada operasi baru yang dapat dimulai setelah panggilan ke close()
ditampilkan.
Gunakan cancel()
jika operasi yang sedang berlangsung dapat dibatalkan sebelum selesai.
Tidak ada operasi baru yang dapat dimulai setelah panggilan ke cancel()
ditampilkan.
Konstruktor class menerima CoroutineContext, bukan CoroutineScope
Jika objek dilarang diluncurkan langsung ke cakupan induk yang disediakan,
kesesuaian CoroutineScope
sebagai parameter konstruktor akan terganggu:
// Don't do this
class MyClass(scope: CoroutineScope) {
private val myJob = Job(parent = scope.`CoroutineContext`[Job])
private val myScope = CoroutineScope(scope.`CoroutineContext` + myJob)
// ... the [scope] constructor parameter is never used again
}
CoroutineScope
menjadi wrapper yang tidak perlu dan menyesatkan yang dalam beberapa kasus penggunaan dapat dibuat hanya untuk diteruskan sebagai parameter konstruktor, hanya untuk dibuang:
// Don't do this; just pass the context
val myObject = MyClass(CoroutineScope(parentScope.`CoroutineContext` + Dispatchers.IO))
Parameter CoroutineContext secara default adalah EmptyCoroutineContext
Jika parameter CoroutineContext
opsional muncul di platform API, nilai defaultnya harus berupa sentinel Empty`CoroutineContext`
. Hal ini memungkinkan
komposisi perilaku API yang lebih baik, karena nilai Empty`CoroutineContext`
dari pemanggil diperlakukan dengan cara yang sama seperti menerima default:
class MyOuterClass(
`CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
private val innerObject = MyInnerClass(`CoroutineContext`)
// ...
}
class MyInnerClass(
`CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
private val job = Job(parent = `CoroutineContext`[Job])
private val scope = CoroutineScope(`CoroutineContext` + job)
// ...
}