Sejak munculnya async
/ await
, Typescript telah menerbitkan banyak artikel yang memuji pendekatan pengembangan ini ( hackernoon , blog.bitsrc.io , habr.com ). Kami menggunakannya sejak awal di sisi klien (ketika ES6 Generator mendukung kurang dari 50% browser). Dan sekarang saya ingin berbagi pengalaman saya, karena eksekusi paralel tidak semua yang baik untuk diketahui di sepanjang jalan ini.
Saya tidak begitu suka artikel terakhir: sesuatu mungkin tidak bisa dimengerti. Sebagian karena fakta bahwa saya tidak dapat memberikan kode kepemilikan - hanya untuk menguraikan pendekatan umum. Oleh karena itu:
- jangan ragu untuk menutup tab tanpa membaca
- jika Anda mengelolanya, mintalah detail yang tidak jelas
- Saya dengan senang hati akan menerima saran dan kritik dari yang paling gigih dan sepenuhnya ditemukan sampai saat ini.
Daftar teknologi inti:
- Proyek ini ditulis terutama dalam naskah menggunakan beberapa perpustakaan Javascript. Perpustakaan utama adalah ExtJS. Ini lebih rendah dalam hal fashionable untuk Bereaksi, tetapi paling cocok untuk produk perusahaan dengan antarmuka yang kaya: banyak komponen yang sudah jadi, tabel yang dirancang dengan baik di luar kotak, ekosistem kaya produk terkait untuk menyederhanakan pengembangan.
- Server multithreaded asinkron.
- RPC melalui Websocket digunakan sebagai transportasi antara klien dan server. Implementasinya mirip dengan .NET WCF.
- Objek apa pun adalah layanan.
- Objek apa pun dapat ditransmisikan oleh nilai dan referensi.
- Antarmuka permintaan data menyerupai GraphQL dari Facebook, hanya pada Filescript.
- Komunikasi dua arah: inisialisasi pembaruan data dapat diluncurkan baik dari klien maupun dari server.
- Kode asinkron ditulis secara berurutan melalui penggunaan fungsi
async
/ await
Typercipt. - API server dihasilkan dalam Script: jika diubah, build akan segera menampilkannya jika terjadi kesalahan.
Apa outputnya
Saya akan memberi tahu Anda bagaimana kami bekerja dengan ini dan apa yang kami lakukan untuk pelaksanaan kode asinkron yang aman dan tidak bersaing: dekorator Typesrcipt kami yang menerapkan fungsi antrian. Dari dasar-dasar ke solusi kondisi ras dan kesulitan lain yang muncul selama proses pengembangan.
Bagaimana data yang diterima dari server disusun
Server mengembalikan objek induk yang berisi data (objek lain, koleksi objek, baris, dll.) Dalam propertinya dalam bentuk grafik. Hal ini disebabkan, antara lain, ke aplikasi itu sendiri:
- itu membuat analisis data / ML grafik diarahkan dari node handler.
- setiap node pada gilirannya dapat berisi grafik yang disematkan sendiri
- grafik memiliki dependensi: node dapat "diwarisi", dan node baru dibuat oleh "kelas" mereka.
Tetapi struktur permintaan dalam bentuk grafik dapat diterapkan di hampir semua aplikasi, dan GraphQL, sejauh yang saya tahu, juga menyebutkan ini dalam spesifikasinya.
Contoh struktur data:
Bagaimana cara klien menerima data
Sederhana: ketika Anda meminta properti dari objek jenis non-skalar, RPC mengembalikan Promise
:
let Nodes = Parent.Nodes;
Sinkronisasi tanpa "Panggilan Balik Neraka".
Untuk mengatur kode asinkron "sekuensial", digunakan fungsi Script async
/ await
:
async function ShowNodes(parent: IParent): Promise<void> {
Tidak masuk akal untuk memikirkannya secara rinci, pada hub sudah ada materi yang cukup rinci . Mereka muncul di belakang pada 2016. Kami telah menggunakan pendekatan ini sejak muncul di cabang fitur dari repositori Typecript, jadi kami telah mendapatkan benjolan untuk waktu yang lama dan sekarang kami bekerja dengan senang hati. Untuk beberapa waktu sekarang, dan dalam produksi.
Secara singkat, esensi bagi mereka yang tidak terbiasa dengan subjek:
Segera setelah Anda menambahkan kata kunci async
ke fungsi, itu akan secara otomatis mengembalikan Promise<_>
. Fitur fungsi tersebut:
- Ekspresi di dalam fungsi
async
dengan await
(yang mengembalikan Promise
) akan menghentikan pelaksanaan fungsi dan melanjutkan setelah menyelesaikan Promise
diharapkan. - Jika pengecualian terjadi dalam fungsi
async
, Promise
dikembalikan akan ditolak dengan pengecualian ini. - Saat mengkompilasi dalam kode Javascript, akan ada generator untuk standar ES6 (
function*
alih-alih async function
dan yield
alih-alih await
) atau kode menakutkan dengan switch
untuk ES5 (mesin negara). await
adalah kata kunci yang menunggu hasil dari janji. Pada saat rapat, selama eksekusi kode, fungsi ShowNodes
berhenti, dan sambil menunggu data, Javascript dapat menjalankan beberapa kode lainnya.
Dalam kode di atas, koleksi memiliki metode forEachParallel
yang memanggil callback asinkron untuk setiap node secara paralel. Pada saat yang sama, await
sebelum Nodes.forEachParallel
akan menunggu semua panggilan balik. Di dalam implementasinya - Promise.all
:
export async function forEachParallel<T>(items: IItemArray<T>, callbackfn: (value: T, index: int, items: IItemArray<T>) => Promise<void | any>, thisArg?: any): Promise<void> { let xCount = items ? await items.Count : 0; if (!xCount) return; let xActions = new Array<Promise<void | any>>(xCount); for (let i = 0; i < xCount; i++) { let xItem = items.Item(i); xActions[i] = ExecuteCallback(xItem, callbackfn, i, items, thisArg); } await Promise.all(xActions); } /** item callbackfn */ async function ExecuteCallback<T>(item: Promise<T> | T, callbackfn: (value: T, index: int, items: IItemArray<T>) => Promise<void | any>, index: int, items: IItemArray<T>, thisArg?: any): Promise<void> { let xItem = await item; await callbackfn.call(thisArg, xItem, index, items); }
Ini adalah gula sintaksis: metode seperti itu harus digunakan tidak hanya untuk koleksi mereka, tetapi juga untuk array Javascript standar.
Fungsi ShowNodes
terlihat sangat tidak optimal: ketika kami meminta entitas lain, kami menunggu setiap kali. Kemudahannya adalah kode tersebut dapat ditulis dengan cepat, sehingga pendekatan ini baik untuk pembuatan prototipe cepat. Di versi final, Anda perlu menggunakan bahasa permintaan untuk mengurangi jumlah panggilan ke server.
Bahasa Pertanyaan
Ada beberapa fungsi yang digunakan untuk "membangun" permintaan data dari server. Mereka βmemberi tahuβ server mana simpul dari grafik data yang akan dikembalikan dalam respons:
selectAsync<T extends IItem>(item: T, properties: () => any[]): Promise<T>; selectAsyncAll<T extends ICollection>(items: T[], properties: () => any[]): Promise<T[]>; select<T>(item: T, properties: () => any[]): T; selectAll<T>(items: T[], properties: () => any[]): T[];
Sekarang mari kita lihat aplikasi fungsi-fungsi ini untuk meminta data tertanam yang diperlukan dengan satu panggilan ke server:
async function ShowNodes(parentPoint: IParent): Promise<void> {
Contoh permintaan yang sedikit lebih kompleks dengan informasi yang tertanam mendalam:
Bahasa permintaan membantu menghindari permintaan yang tidak perlu ke server. Tetapi kodenya tidak pernah sempurna, dan pasti akan berisi beberapa permintaan kompetitif dan, sebagai hasilnya, kondisi balapan.
Kondisi dan Solusi Ras
Karena kami berlangganan acara server dan menulis kode dengan sejumlah besar permintaan asinkron, kondisi balapan dapat terjadi ketika fungsi FuncOne
async
terganggu, menunggu Promise
. Pada saat ini, peristiwa server (atau dari aksi pengguna berikutnya) dapat datang dan, setelah dijalankan secara kompetitif, ubah model pada klien. Kemudian FuncOne
setelah menyelesaikan janji, dapat mengubah, misalnya, ke sumber daya yang sudah dihapus.
Bayangkan situasi yang disederhanakan: objek IParent
memiliki delegasi server IParent
.
Parent.OnSynchronize.AddListener(async function(): Promise<void> {
Disebut ketika daftar node INodes
di server diperbarui. Kemudian, dalam skenario berikut, kondisi balapan dimungkinkan:
- Kami menyebabkan penghapusan simpul yang tidak sinkron dari klien, menunggu penyelesaian untuk menghapus objek klien
async function OnClickRemoveNode(node: INode): Promise<void> { let removedOnServer: boolean = await Parent.RemoveNode(node);
- Melalui
Parent.OnSynchronize
, pembaruan acara daftar simpul terjadi. Parent.OnSynchronize
diproses dan menghapus objek klien.async OnClickRemoveNode()
terus dijalankan setelah await
pertama dan upaya dilakukan untuk menghapus objek klien yang sudah dihapus.
Anda bisa melakukan pengecekan tentang keberadaan objek klien di OnClickRemoveNode
. Ini adalah contoh yang disederhanakan dan di dalamnya pemeriksaan serupa adalah normal. Tetapi bagaimana jika rantai panggilan lebih rumit? Oleh karena itu, menggunakan pendekatan yang sama setelah setiap await
adalah praktik yang buruk:
- Kode yang kembung sangat rumit untuk didukung dan diperluas.
- Kode tidak berfungsi sebagaimana dimaksud: penghapusan di
OnClickRemoveNode
, dan penghapusan aktual objek klien terjadi di tempat lain. Seharusnya tidak ada pelanggaran terhadap urutan yang ditentukan oleh pengembang, jika tidak akan ada kesalahan regresi. - Ini tidak cukup andal: jika Anda lupa melakukan pemeriksaan di suatu tempat, maka akan ada kesalahan. Bahayanya adalah, pertama-tama, bahwa pemeriksaan yang dilupakan mungkin tidak menyebabkan kesalahan secara lokal dan dalam lingkungan pengujian, dan bagi pengguna dengan penundaan jaringan yang lebih lama akan terjadi.
- Dan jika pengontrol yang dimiliki penangan ini dapat dihancurkan? Setelah masing-masing
await
untuk memeriksa kehancurannya?
Pertanyaan lain muncul: bagaimana jika ada banyak metode kompetitif serupa? Bayangkan ada lebih banyak:
- Menambahkan Node
- Pembaruan node
- Tambah / Hapus Tautan
- Metode Konversi Beberapa Node
- Perilaku aplikasi yang kompleks: kami mengubah status satu simpul dan server mulai memperbarui simpul yang bergantung padanya.
Diperlukan implementasi arsitektur, yang pada prinsipnya menghilangkan kemungkinan kesalahan karena kondisi ras, aksi pengguna paralel, dll. Solusi yang tepat untuk menghilangkan perubahan simultan model dari klien atau server adalah dengan mengimplementasikan bagian penting dengan antrian panggilan. Dekorator pengetik naskah akan berguna di sini untuk menandai fungsi pengontrol asinkron yang kompetitif secara deklaratif.
Kami menguraikan persyaratan dan fitur utama dari dekorator tersebut:
- Di dalam, antrian panggilan ke fungsi asinkron harus diterapkan. Bergantung pada jenis dekorator, panggilan fungsi dapat antri atau ditolak jika ada panggilan lain di dalamnya.
- Fungsi yang ditandai akan memerlukan konteks eksekusi untuk mengikat ke antrian. Anda harus membuat antrian secara eksplisit, atau melakukannya secara otomatis berdasarkan Tampilan yang dimiliki pengontrol.
- Informasi diperlukan tentang penghancuran instance controller (misalnya, properti
IsDestroyed
). Untuk mencegah dekorator membuat panggilan antrian setelah pengontrol dihancurkan. - Untuk pengontrol tampilan, kami menambahkan fungsionalitas menerapkan masker transparan untuk mengecualikan tindakan pada saat antrian dieksekusi dan secara visual menunjukkan pemrosesan sedang berlangsung.
- Semua dekorator harus diakhiri dengan panggilan ke
Promise.done()
. Dalam metode ini, Anda perlu menerapkan handler
pengecualian yang tidak ditangani. Suatu hal yang sangat berguna:
Sekarang kami memberikan daftar perkiraan dekorator tersebut dengan deskripsi dan kemudian menunjukkan bagaimana mereka dapat diterapkan.
@Lock @LockQueue @LockBetween @LockDeferred(300)
Uraiannya cukup abstrak, tetapi begitu Anda melihat contoh penggunaan dengan penjelasan, semuanya akan menjadi lebih jelas:
class GraphController implements ILockTarget { private View: IView; public GetControllerView(): IView { return this.View; } @Lock private async OnClickRemoveNode(): Promise<void> { ... } @Lock private async OnClickRemoveLink(): Promise<void> { ... } @Lock private async OnClickAddNewNode(): Promise<void> { ... } @LockQueue private async OnServerUpdateNode(): Promise<void> { ... } @LockQueue private async OnServerAddLink(): Promise<void> { ... } @LockQueue private async OnServerAddNode(): Promise<void> { ... } @LockQueue private async OnServerRemoveNode(): Promise<void> { ... } @LockBetween private async OnServerSynchronize(): Promise<void> { ... } @LockQueue private async OnServerUpdateNodeStatus(): Promise<void> { ... } @LockDeferred(300) private async OnSearchFieldChange(): Promise<void> { ... } }
Sekarang kita akan menganalisis beberapa skenario tipikal dari kemungkinan kesalahan dan penghapusannya oleh dekorator:
- Pengguna memulai tindakan:
OnClickRemoveNode
, OnClickRemoveLink
. Untuk pemrosesan yang tepat, perlu bahwa tidak ada penangan pelaksana lainnya dalam antrian (baik klien atau server). Kalau tidak, misalnya, kesalahan seperti itu dimungkinkan:
- Model pada klien masih diperbarui ke kondisi server saat ini
- Kami memulai penghapusan objek sebelum pembaruan selesai (ada penangan
OnServerSynchronize
dalam antrian). Tetapi objek ini sebenarnya sudah tidak ada lagi - hanya sinkronisasi penuh belum selesai dan masih ditampilkan pada klien.
Oleh karena itu, semua tindakan yang dilakukan oleh pengguna, Penghias Lock
harus menolak jika ada penangan lain dalam antrian dengan konteks antrian yang sama. Mengingat bahwa server tidak sinkron, ini sangat penting. Ya, Websocket mengirimkan permintaan secara berurutan, tetapi jika klien memecah urutan, kami mendapatkan kesalahan di server.
- Kami memulai penambahan node:
OnClickAddNewNode
. OnServerSynchronize
, peristiwa OnServerAddNode
berasal dari server.
OnClickAddNewNode
mengambil antrian (jika ada sesuatu di dalamnya, OnClickAddNewNode
Lock
metode ini akan menolak panggilan)OnServerSynchronize
, OnServerAddNode
, dijalankan secara berurutan setelah OnClickAddNewNode
, tidak bersaing dengannya.
- Antrian memiliki
OnServerUpdateNode
dan OnServerUpdateNode
. Misalkan selama eksekusi yang pertama, pengguna menutup GraphController
. Maka panggilan kedua ke OnServerUpdateNode
tidak boleh dilakukan secara otomatis agar tidak mengambil tindakan pada controller yang hancur, yang dijamin akan menyebabkan kesalahan. Untuk ini, antarmuka ILockTarget
telah IsDestroyed
- dekorator memeriksa bendera tanpa mengeksekusi handler berikutnya dari antrian.
Keuntungan: tidak perlu menulis if (!this.IsDestroyed())
setelah masing-masing await
. - Perubahan ke beberapa node dimulai.
OnServerSynchronize
, peristiwa OnServerUpdateNode
berasal dari server. Eksekusi kompetitif mereka akan menyebabkan kesalahan yang tidak dapat direproduksi. Tapi sejak itu LockQueue
mereka ditandai oleh LockBetween
dan LockBetween
, mereka akan dieksekusi secara berurutan. - Bayangkan bahwa simpul dapat memiliki grafik simpul bersarang di dalamnya.
GraphController #1
, β GraphController #2
. , GraphController
- , ( β ), .. . :
OnSearchFieldChange
, . - . @LockDeferred(300)
300 : , , 300 . , . :
- , 500 , . β
OnSearchFieldChange
, . OnSearchFieldChange
β , .
- Deadlock:
Handler1
, , await
Handler2
, LockQueue
, Handler2
β Handler1
. - , View . : , β .
, , . :
- -
<Class>
. <Method>
=> <Time>
( ). - .
- .
, , , . ? ? :
class GraphController implements ILockTarget { private View: IView; public GetControllerView(): IView { return this.View; } @Lock private async RunBigDataCalculations(): Promise<void> { await Start(); await UpdateSmth(); await End(); await CleanUp(); } @LockQueue private async OnChangeNodeState(node: INode): Promise<void> { await GetNodeData(node); await UpdateNode(node); } }
:
RunBigDataCalculations
.await Start();
- / ( )
await Start();
, await UpdateSmth();
.
:
RunBigDataCalculations
.OnChangeNodeState
, (.. ).await GetNodeData(node);
- / ( )
await GetNodeData(node);
, await UpdateNode(node);
.
- . :
export interface IQueuedDisposableLockTarget extends ILockTarget { IsDisposing(): boolean; SetDisposing(): void; }
function QueuedDispose(controller: IQueuedDisposableLockTarget): void {
, . QueuedDispose
:
- . .
QueuedDispose
controller
. β ExtJS .
, , .. . , ? , .
, :
vk.com
Telegram