API non-pemblokiran meminta pekerjaan untuk dilakukan, lalu memberikan kontrol kembali ke thread panggilan sehingga dapat melakukan pekerjaan lain sebelum menyelesaikan operasi yang diminta. API ini berguna untuk kasus saat pekerjaan yang diminta mungkin sedang berlangsung atau mungkin memerlukan penantian hingga I/O atau IPC selesai, ketersediaan resource sistem yang sangat diperebutkan, atau input pengguna sebelum pekerjaan dapat dilanjutkan. API yang dirancang dengan baik menyediakan cara untuk membatalkan operasi yang sedang berlangsung dan menghentikan pekerjaan agar tidak dilakukan atas nama pemanggil asli, sehingga menjaga kesehatan sistem dan masa pakai baterai saat operasi tidak lagi diperlukan.
API asinkron adalah salah satu cara untuk mencapai perilaku non-pemblokiran. 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 serentak, dengan operasi ke-N harus dimulai sebelum operasi ke-N-1 selesai.
- Menghindari pemblokiran thread panggilan hingga operasi selesai.
Kotlin sangat mendukung konkurensi terstruktur, serangkaian prinsip dan API yang dibuat berdasarkan fungsi penangguhan yang memisahkan eksekusi kode sinkron dan asinkron dari perilaku pemblokiran thread. Fungsi penangguhan bersifat non-pemblokiran dan sinkron.
Fungsi penangguhan:
- Jangan memblokir thread panggilannya, tetapi hasilkan thread eksekusinya sebagai detail implementasi sambil menunggu hasil operasi yang dijalankan di tempat lain.
- Jalankan secara sinkron dan tidak mengharuskan pemanggil API non-blocking untuk melanjutkan eksekusi secara serentak dengan tugas non-blocking yang dimulai oleh panggilan API.
Halaman ini menjelaskan dasar minimum ekspektasi yang dapat dipegang developer dengan aman saat menggunakan API asinkron dan non-blocking, diikuti dengan serangkaian resep untuk menulis API yang memenuhi ekspektasi ini dalam bahasa Kotlin atau Java, di platform Android atau library Jetpack. Jika ragu, pertimbangkan ekspektasi developer sebagai persyaratan untuk platform API baru.
Ekspektasi developer untuk API asinkron
Perkiraan berikut ditulis dari sudut pandang API yang tidak ditangguhkan kecuali jika dinyatakan lain.
API yang menerima callback biasanya asinkron
Jika API menerima callback yang tidak didokumentasikan untuk hanya dipanggil di tempat, (yaitu, hanya dipanggil oleh thread panggilan sebelum panggilan API itself ditampilkan,) API diasumsikan asinkron dan API tersebut harus memenuhi semua ekspektasi lainnya yang didokumentasikan di bagian berikut.
Contoh callback yang hanya pernah dipanggil di tempat adalah fungsi peta atau filter tingkat tinggi yang memanggil pemetaan 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. Panggilan API asinkron harus selalu aman kapan saja, dan memanggil API asinkron tidak boleh menghasilkan frame yang janky atau ANR.
Banyak operasi dan sinyal siklus proses dapat dipicu oleh platform atau
library on-demand, dan mengharapkan developer memiliki pengetahuan global tentang semua
situs panggilan potensial 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 melakukan operasi startup
satu kali di sini, dan ini mungkin berada di jalur kode kritis untuk menghasilkan
frame animasi yang bebas jank. Developer harus selalu yakin bahwa
memanggil setiap API asinkron sebagai respons terhadap jenis callback siklus proses ini
tidak akan menyebabkan frame yang lambat.
Hal ini menyiratkan bahwa pekerjaan yang dilakukan oleh API asinkron sebelum ditampilkan harus sangat ringan; membuat catatan permintaan dan callback terkait, serta mendaftarkannya ke mesin eksekusi yang paling banyak melakukan pekerjaan. Jika pendaftaran untuk operasi asinkron memerlukan IPC, implementasi API harus mengambil tindakan apa pun yang diperlukan untuk memenuhi ekspektasi developer ini. Hal ini dapat mencakup satu atau beberapa hal berikut:
- Mengimplementasikan IPC yang mendasarinya sebagai panggilan binder satu arah
- Melakukan panggilan binder dua arah ke server sistem tempat menyelesaikan pendaftaran tidak memerlukan 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 menampilkan argumen yang tidak valid
Async API harus melaporkan semua hasil operasi yang diminta ke callback yang disediakan. Hal ini memungkinkan developer menerapkan satu jalur kode untuk penanganan error dan keberhasilan.
API asinkron dapat memeriksa argumen untuk null dan menampilkan NullPointerException
, atau
memeriksa apakah argumen yang diberikan berada dalam rentang yang valid dan menampilkan
IllegalArgumentException
. Misalnya, untuk fungsi yang menerima float
dalam rentang 0
hingga 1f
, fungsi tersebut 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 alfanumerik saja. (Ingat 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 disediakan. Hal ini mencakup, tetapi tidak terbatas pada:
- Kegagalan terminal operasi yang diminta
- Pengecualian keamanan untuk otorisasi atau izin yang tidak ada dan 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
- Penghentian binder atau proses jarak jauh tidak tersedia
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 akan menandakan dua hal:
Referensi hard ke callback yang disediakan oleh pemanggil harus dirilis
Callback yang disediakan ke API asinkron dapat berisi referensi hard ke grafik objek besar, dan pekerjaan yang sedang berlangsung yang menyimpan referensi hard ke callback tersebut dapat mencegah grafik objek tersebut di-garbage collection. Dengan merilis referensi callback ini saat pembatalan, grafik objek ini dapat memenuhi syarat untuk pengumpulan sampah jauh lebih cepat daripada jika pekerjaan diizinkan untuk berjalan hingga selesai.
Mesin eksekusi yang melakukan pekerjaan untuk pemanggil dapat menghentikan pekerjaan tersebut
Pekerjaan yang dimulai oleh panggilan API asinkron dapat menimbulkan biaya tinggi dalam penggunaan daya atau resource sistem lainnya. API yang memungkinkan pemanggil untuk memberikan sinyal saat pekerjaan ini tidak lagi diperlukan mengizinkan penghentian pekerjaan tersebut sebelum dapat menggunakan resource sistem lebih lanjut.
Pertimbangan khusus untuk aplikasi yang di-cache atau dibekukan
Saat mendesain API asinkron yang callback-nya berasal dari proses sistem dan dikirim ke aplikasi, pertimbangkan hal berikut:
- Proses dan siklus proses aplikasi: proses aplikasi penerima mungkin dalam status di-cache.
- Pembeku aplikasi dalam cache: proses aplikasi penerima mungkin dibekukan.
Saat proses aplikasi memasuki status cache, artinya proses tersebut tidak secara aktif menghosting komponen yang terlihat oleh pengguna seperti aktivitas dan layanan. Aplikasi disimpan dalam memori jika dapat dilihat pengguna lagi, tetapi sementara itu tidak boleh melakukan pekerjaan. Dalam sebagian besar kasus, Anda harus menjeda pengiriman callback aplikasi saat aplikasi tersebut memasuki status cache dan melanjutkan saat aplikasi keluar dari status cache, agar tidak memicu pekerjaan dalam proses aplikasi yang di-cache.
Aplikasi yang di-cache juga dapat dibekukan. Saat dibekukan, aplikasi tidak menerima waktu CPU dan tidak dapat melakukan pekerjaan apa pun. Setiap panggilan ke callback terdaftar aplikasi tersebut akan di-buffer dan dikirim saat aplikasi dicairkan.
Transaksi yang di-buffer ke callback aplikasi mungkin sudah tidak berlaku pada saat aplikasi di-unfreeze dan memprosesnya. Buffer bersifat terbatas, dan jika kelebihan kapasitas akan menyebabkan aplikasi penerima error. Untuk menghindari aplikasi yang kewalahan dengan peristiwa yang sudah tidak berlaku atau melebihi buffer-nya, jangan kirim callback aplikasi saat prosesnya dibekukan.
Sedang ditinjau:
- Anda harus mempertimbangkan menjeda callback aplikasi pengiriman 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 dicairkan:
IBinder binder = <...>;
binder.addFrozenStateChangeCallback(executor, callback);
Strategi untuk melanjutkan callback aplikasi pengiriman
Baik Anda menjeda pengiriman callback aplikasi saat aplikasi memasuki status cache atau status beku, saat aplikasi keluar dari status masing-masing, Anda harus melanjutkan pengiriman callback terdaftar aplikasi setelah aplikasi keluar dari status masing-masing hingga aplikasi membatalkan pendaftaran callback-nya atau proses aplikasi berhenti.
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 tidak
mengirim 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()
hanya dipanggil jika prosesnya tidak dibekukan.
Aplikasi sering menyimpan update yang diterima menggunakan callback sebagai snapshot status terbaru. Pertimbangkan API hipotetis untuk aplikasi guna memantau persentase baterai yang tersisa:
interface BatteryListener {
void onBatteryPercentageChanged(int newPercentage);
}
Pertimbangkan skenario saat beberapa peristiwa perubahan status terjadi saat aplikasi dibekukan. Saat aplikasi dicairkan, Anda hanya boleh mengirimkan status terbaru ke aplikasi dan menghapus perubahan status lama lainnya. Pengiriman ini akan segera terjadi saat aplikasi dicairkan sehingga 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 dikirim ke aplikasi sehingga aplikasi tidak perlu diberi tahu tentang nilai yang sama setelah dicairkan.
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 melanjutkan, sebaiknya beri tahu aplikasi tentang jaringan lama yang hilang, jaringan baru yang tersedia, dan jaringan yang ada yang statusnya telah berubah - dalam urutan ini.
Jangan beri tahu aplikasi tentang jaringan yang tersedia, lalu hilang saat callback dijeda. Aplikasi tidak boleh menerima laporan lengkap peristiwa yang terjadi saat aplikasi dibekukan, dan dokumentasi API tidak boleh berjanji untuk mengirim streaming peristiwa tanpa gangguan di luar status siklus proses eksplisit. Dalam contoh ini, jika aplikasi perlu terus memantau ketersediaan jaringan, aplikasi harus tetap dalam status siklus proses yang mencegahnya di-cache atau dibekukan.
Dalam peninjauan, Anda harus menggabungkan peristiwa yang telah terjadi setelah menjeda dan sebelum melanjutkan notifikasi serta mengirimkan status terbaru ke callback aplikasi yang terdaftar secara ringkas.
Pertimbangan untuk dokumentasi developer
Pengiriman peristiwa asinkron dapat tertunda, baik karena pengirim menjeda pengiriman 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.
Melarang 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 sudah memahami konkurensi terstruktur Kotlin mengharapkan perilaku berikut dari API penangguhan:
Fungsi penangguhan harus menyelesaikan semua pekerjaan terkait sebelum menampilkan atau menampilkan
Hasil operasi non-pemblokiran ditampilkan sebagai nilai hasil fungsi normal, dan error dilaporkan dengan menampilkan 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 pekerjaan terkait sebelum ditampilkan, sehingga tidak boleh memanggil callback yang disediakan atau parameter fungsi lainnya atau mempertahankan referensi ke fungsi tersebut setelah fungsi penangguhan ditampilkan.
Fungsi penangguhan yang menerima parameter callback harus mempertahankan konteks, kecuali jika didokumentasikan sebaliknya
Memanggil fungsi dalam fungsi penangguhan akan menyebabkannya berjalan di
CoroutineContext
pemanggil. Karena fungsi penangguhan harus menyelesaikan semua
pekerjaan terkait sebelum menampilkan atau menampilkan, dan hanya boleh memanggil parameter
callback di tempat, ekspektasi defaultnya adalah bahwa callback tersebut
juga berjalan di CoroutineContext
panggilan menggunakan dispatcher terkait. Jika
tujuan API adalah menjalankan callback di luar CoroutineContext
pemanggil, 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
. Implementasi 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 menghasilkan 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 pekerjaan 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 harus mengekspos fungsi pemblokiran yang mendasarinya dan merekomendasikan agar
developer yang memanggil menggunakan panggilan mereka sendiri ke withContext untuk mengarahkan pekerjaan ke
dispatcher yang dipilih.
Class yang meluncurkan coroutine
Class yang meluncurkan coroutine harus memiliki CoroutineScope
untuk melakukan
operasi peluncuran tersebut. Menghormati 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 memiliki MyClass
yang mengelola
CoroutineScope
. Serialisasi pemrosesan permintaan menjadi lebih sederhana dan
status sering kali dapat ada sebagai variabel lokal handleRequests
, bukan sebagai properti
class yang akan memerlukan sinkronisasi tambahan.
Class yang mengelola coroutine harus mengekspos metode tutup dan batalkan
Class yang meluncurkan coroutine sebagai detail implementasi harus menawarkan cara untuk
menutup tugas serentak yang sedang berlangsung dengan rapi sehingga tidak membocorkan
pekerjaan serentak yang tidak terkontrol ke dalam cakupan induk. Biasanya, ini berupa
pembuatan 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 pekerjaan serentak yang belum selesai yang dilakukan oleh objek.
(Tindakan ini dapat mencakup pekerjaan pembersihan yang dilakukan dengan membatalkan operasi.)
suspend fun join() {
myJob.join()
}
Penamaan operasi terminal
Nama yang digunakan untuk metode yang mematikan tugas serentak yang dimiliki oleh objek yang masih berlangsung harus mencerminkan kontrak perilaku tentang cara penonaktifan 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 rusak:
// 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
dihapus:
// Don't do this; just pass the context
val myObject = MyClass(CoroutineScope(parentScope.`CoroutineContext` + Dispatchers.IO))
Parameter CoroutineContext secara default ditetapkan ke EmptyCoroutineContext
Saat 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)
// ...
}