Logika bisnis asinkron hari ini

Singkatnya:


  • Buktinya sudah diimplementasikan dalam C ++ , JS dan PHP , cocok untuk Java .
  • Lebih cepat dari coroutine dan Promise, lebih banyak fitur.
  • Itu tidak memerlukan tumpukan perangkat lunak terpisah.
  • Berteman dengan semua alat keamanan dan debugging.
  • Ia bekerja pada arsitektur apa pun dan tidak memerlukan flag kompiler khusus.





Lihatlah ke belakang


Pada awal komputer, ada aliran kontrol tunggal dengan memblokir input-output. Kemudian interupsi besi ditambahkan ke dalamnya. Sekarang Anda dapat secara efektif menggunakan perangkat yang lambat dan tidak dapat diprediksi.


Dengan pertumbuhan kemampuan besi dan ketersediaannya yang rendah, menjadi perlu untuk melakukan beberapa tugas secara bersamaan, yang memberikan dukungan perangkat keras. Jadi ada proses terisolasi dengan interupsi yang disarikan dari besi dalam bentuk sinyal.


Tahap evolusi berikutnya adalah multithreading, yang diimplementasikan atas dasar proses yang sama, tetapi dengan akses bersama ke memori dan sumber daya lainnya. Pendekatan ini memiliki keterbatasan dan overhead yang signifikan untuk beralih ke OS yang aman.


Untuk komunikasi antara proses dan bahkan mesin yang berbeda, abstraksi Janji / Masa Depan diusulkan 40+ tahun yang lalu.


Antarmuka pengguna dan masalah klien 10K yang saat ini menggelikan telah menyebabkan masa kejayaan pendekatan Event Loop, Reactor, dan Proactor, yang lebih berorientasi pada peristiwa daripada logika bisnis yang jelas dan konsisten.


Akhirnya, kami sampai pada coroutine modern (coroutine), yang pada dasarnya adalah emulasi aliran di atas abstraksi yang dijelaskan di atas dengan keterbatasan teknis yang sesuai dan transfer kontrol deterministik.


Untuk menyampaikan peristiwa, hasil, dan pengecualian, semuanya kembali ke konsep Janji / Masa Depan yang sama. Beberapa kantor memutuskan untuk memberi nama sedikit berbeda - "Tugas".


Pada akhirnya, mereka menyembunyikan semuanya dalam paket async/await indah, yang membutuhkan dukungan kompiler atau penerjemah tergantung pada teknologinya.


Masalah dengan situasi logika bisnis asinkron saat ini


Anggap saja coroutine dan Promise, yang dihiasi async/await , sebagai keberadaan masalah dalam pendekatan yang lebih lama menegaskan proses evolusi itu sendiri.


Kedua istilah ini tidak identik. Sebagai contoh, dalam ECMAScript tidak ada coroutine, tetapi ada bantuan sintaksis untuk menggunakan Promise , yang pada gilirannya hanya mengatur pekerjaan dengan neraka panggilan balik. Bahkan, mesin scripting seperti V8 melangkah lebih jauh dan melakukan optimasi khusus untuk fungsi dan panggilan async/await murni.


Para ahli co_async/co_await yang tidak termasuk dalam C ++ 17 di sini tentang sumber daya , tetapi tekanan dari coroutine raksasa perangkat lunak dapat muncul dalam standar persis dalam bentuknya. Sementara itu, solusi yang diakui secara tradisional adalah Boost.Context , Boost.Fiber dan Boost.Coroutine2 .


Di Jawa, masih belum ada async/await di tingkat bahasa, tetapi ada solusi seperti EA Async , yang, seperti Boost.Context, perlu disesuaikan untuk setiap versi JVM dan byte kode.


Go memiliki coroutine sendiri, tetapi jika Anda hati-hati melihat artikel dan laporan bug proyek terbuka, ternyata di sini semuanya tidak begitu lancar. Mungkin kehilangan antarmuka coroutine sebagai entitas yang dikelola bukanlah ide yang baik.


Opini Penulis: coroutine bare-metal berbahaya


Secara pribadi, penulis memiliki sedikit terhadap coroutine dalam bahasa yang dinamis, tetapi ia sangat waspada terhadap godaan dengan tumpukan di tingkat kode mesin.


Beberapa poin:


  1. Diperlukan tumpukan:
    • tumpukan di heap memiliki sejumlah kelemahan: masalah penentuan tepat waktu meluap, kerusakan oleh tetangga dan masalah keandalan / keamanan lainnya,
    • tumpukan aman memerlukan setidaknya satu halaman memori fisik, satu halaman bersyarat dan overhead tambahan untuk setiap panggilan ke fungsi async : 4 + KB (minimum) + peningkatan batas sistem,
    • pada akhirnya, mungkin sebagian besar memori yang dialokasikan untuk tumpukan tidak digunakan selama downtime coroutine.
  2. Kita perlu menerapkan logika kompleks untuk menyelamatkan, memulihkan, dan menghapus status coroutine:
    • untuk setiap kasus arsitektur prosesor (bahkan model) dan antarmuka biner (ABI): contoh ,
    • fitur arsitektur baru atau opsional menimbulkan masalah yang berpotensi laten (misalnya, Intel TSX, co-prosesor ARM atau MIPS),
    • potensi masalah lain karena sistem tertutup dokumentasi tertutup (Dokumentasi Boost merujuk pada ini).
  3. Masalah potensial dengan alat analisis dinamis dan keamanan secara umum:
    • misalnya, integrasi dengan Valgrind diperlukan semua karena tumpukan lompatan yang sama,
    • sulit untuk berbicara tentang antivirus, tetapi mungkin mereka tidak begitu suka pada contoh masalah dengan JVM di masa lalu,
    • Saya yakin jenis serangan baru akan muncul dan kerentanan yang terkait dengan penerapan coroutine akan terungkap.

Pendapat penulis: generator dan yield kejahatan mendasar


Tema pihak ketiga yang tampaknya terkait langsung dengan konsep coroutine dan properti "terus".


Singkatnya, iterator lengkap harus ada untuk koleksi apa pun. Mengapa membuat masalah iterator-generator yang dipangkas tidak jelas. Misalnya, case dengan range() dalam Python lebih merupakan pamer eksklusif daripada alasan untuk komplikasi teknis.


Jika case adalah generator tanpa batas, maka logika implementasinya adalah elementer. Mengapa membuat kesulitan teknis tambahan untuk mendorong siklus berkelanjutan tanpa akhir.


Satu-satunya pembenaran yang masuk akal yang kemudian muncul yang diberikan oleh pendukung coroutine adalah segala macam pengurai aliran dengan kontrol terbalik. Bahkan, ini adalah kasus khusus yang sempit untuk memecahkan masalah tunggal di tingkat perpustakaan, bukan logika bisnis aplikasi. Pada saat yang sama ada solusi yang elegan, sederhana dan lebih deskriptif melalui mesin negara yang terbatas. Area masalah teknis ini jauh lebih kecil daripada area logika bisnis biasa.


Bahkan, masalah yang harus dipecahkan diperoleh dari jari dan membutuhkan upaya yang relatif serius untuk implementasi awal dan dukungan jangka panjang. Sedemikian rupa sehingga beberapa proyek dapat memperkenalkan larangan penggunaan coroutine tingkat kode mesin mengikuti contoh larangan goto atau penggunaan alokasi memori dinamis dalam industri individu.


Pendapat penulis: Model Promyn async/await ECMAScript lebih dapat diandalkan, tetapi membutuhkan adaptasi


Tidak seperti melanjutkan coroutine, dalam model ini potongan-potongan kode diam-diam dibagi menjadi blok non-interruptible yang dirancang sebagai fungsi anonim. Dalam C ++, ini tidak sepenuhnya cocok karena kekhasan manajemen memori, contoh:


 struct SomeObject { using Value = std::vector<int>; Promise funcPromise() { return Promise.resolved(value_); } void funcCallback(std::function<void()> &&cb, const Value& val) { somehow_call_later(cb); } Value value_; }; Promise example() { SomeObject some_obj; return some_obj.funcPromise() .catch([](const std::exception &e){ // ... }) .then([&](SomeObject::value &&val){ return Promise([&](Resolve&& resolve, Reject&&){ some_obj.funcCallback(resolve, val); }); }); } 

Pertama, some_obj akan dihancurkan ketika keluar dari example() dan sebelum memanggil fungsi lambda.


Kedua, fungsi lambda dengan menangkap variabel atau referensi adalah objek dan diam-diam menambahkan copy / move, yang dapat secara negatif mempengaruhi kinerja dengan sejumlah besar tangkapan dan kebutuhan untuk mengalokasikan memori pada heap selama penghapusan tipe pada std::function biasa.


Ketiga, antarmuka Promise itu sendiri dikandung pada konsep "janji" dari hasil, daripada pelaksanaan logika bisnis yang konsisten.


Solusi skema TIDAK optimal mungkin terlihat seperti ini:


 Promise example() { struct LocalContext { SomeObject some_obj; }; auto ctx = std::make_shared<LocalContext>(); return some_obj.funcPromise() .catch([](const std::exception &e){ // ... }) .then([ctx](SomeObject::Value &&val){ struct LocalContext2 { LocalContext2(std::shared_ptr<LocalContext> &&ctx, SomeObject::Value &&val) : ctx(ctx), val(val) {} std::shared_ptr<LocalContext> ctx; SomeObject::Value val; }; auto ctx2 = std::make_shared<LocalContext2>( std::move(ctx), std::forward<SomeObject::Value>(val) ); return Promise([ctx2](Resolve&& resolve, Reject&&){ ctx2->ctx->some_obj.funcCallback([ctx2, resolve](){ resolve(); }, val); }); }); } 

Catatan: std::move bukannya std::shared_ptr tidak cocok karena ketidakmampuan untuk mentransfer ke beberapa lambda sekaligus dan pertumbuhan ukurannya.


Dengan tambahan async/await kengerian asinkron datang dalam kondisi yang dapat dicerna:


 async void example() { SomeObject some_obj; try { SomeObject::Value val = await some_obj.func(); } catch (const std::exception& e) ( // ... } // Capture "async context" return Promise([async](Resolve&& resolve, Reject&&){ some_obj.funcCallback([async](){ resolve(); }, val); }); } 

Pendapat penulis: perencana coroutine adalah sebuah kegagalan


Beberapa kritik menyebut kurangnya penjadwal dan penggunaan sumber daya prosesor yang β€œtidak adil” merupakan masalah. Mungkin masalah yang lebih serius adalah lokalitas data dan penggunaan cache prosesor yang efisien.


Pada masalah pertama: penentuan prioritas pada tingkat masing-masing coroutine terlihat seperti biaya overhead yang besar. Sebaliknya, mereka dapat dioperasikan pada kesamaan untuk tugas terpadu tertentu. Inilah yang dilakukan arus lalu lintas.


Ini dimungkinkan dengan membuat instance Event Loop terpisah dengan utas "besi" sendiri dan perencanaan di tingkat OS. Opsi kedua adalah menyinkronkan coroutine dengan primitif (Mutex, Throttle) yang relatif primitif dalam hal kompetisi dan / atau kinerja.


Pemrograman asinkron tidak membuat sumber daya prosesor kenyal dan membutuhkan batasan yang benar-benar normal pada jumlah tugas yang diproses secara bersamaan dan batas waktu eksekusi total.


Perlindungan terhadap pemblokiran yang lama pada satu coroutine memerlukan langkah-langkah yang sama dengan panggilan balik - untuk menghindari pemblokiran panggilan sistem dan siklus pemrosesan data yang panjang.


Masalah kedua membutuhkan penelitian, tetapi setidaknya coroutine menumpuk sendiri dan rincian implementasi Future / Promise sudah melanggar lokalitas data. Ada peluang untuk mencoba melanjutkan eksekusi coroutine yang sama jika Future sudah penting. Diperlukan mekanisme tertentu untuk menghitung waktu eksekusi atau jumlah kelanjutan tersebut untuk mencegah satu coroutine menangkap seluruh waktu prosesor. Ini mungkin tidak memberikan hasil, atau memberikan hasil yang sangat berlipat ganda tergantung pada ukuran cache prosesor dan jumlah utas.


Ada juga poin ketiga - banyak implementasi penjadwalan coroutine memungkinkan mereka untuk dijalankan pada core prosesor yang berbeda, yang sebaliknya menambah masalah karena sinkronisasi wajib ketika mengakses sumber daya bersama. Dalam kasus aliran Peristiwa tunggal, sinkronisasi seperti itu hanya diperlukan pada tingkat logis, sejak itu Setiap blok panggilan balik yang sinkron dijamin bekerja tanpa balapan dengan yang lain.


Pendapat penulis: semuanya baik-baik saja


Kehadiran utas dalam sistem operasi modern tidak meniadakan penggunaan proses individu. Juga, memproses sejumlah besar klien di Event Loop tidak meniadakan penggunaan benang "besi" yang terisolasi untuk kebutuhan lain.


Bagaimanapun, coroutine dan berbagai varian Event Loops menyulitkan proses debugging tanpa dukungan yang diperlukan dalam alat, dan dengan variabel lokal pada tumpukan coroutine, semuanya menjadi lebih sulit - praktis tidak ada cara untuk mendapatkannya.





FutoIn AsyncSteps - sebuah alternatif untuk coroutine


Kami mengambil sebagai dasar pola Perulangan Kejadian dan organisasi skema panggilan balik yang sudah mapan sesuai dengan tipe Janji ECMAScript (JavaScript).


Dalam hal perencanaan eksekusi, kami tertarik pada kegiatan berikut dari Perulangan Kejadian:


  1. Handle immediate(callack) membutuhkan tumpukan panggilan yang bersih.
  2. Callback Handle deferred(delay, callback) .
  3. Batalkan callback handle.cancel() .

Jadi kita mendapatkan antarmuka yang disebut AsyncTool , yang dapat diimplementasikan dalam banyak cara, termasuk di atas perkembangan yang sudah terbukti. Dia tidak memiliki hubungan langsung dengan penulisan logika bisnis, jadi kami tidak akan membahas lebih jauh.


Pohon langkah:


Dalam konsep AsyncSteps, pohon abstrak langkah sinkron berbaris dan dieksekusi dengan masuk jauh ke dalam urutan pembuatan. Langkah-langkah dari setiap level yang lebih dalam ditetapkan secara dinamis saat bagian tersebut selesai.


Semua interaksi terjadi melalui antarmuka AsyncSteps tunggal, yang, menurut AsyncSteps , dilewatkan sebagai parameter pertama untuk setiap langkah. Sesuai konvensi, nama parameternya asi atau tidak digunakan lagi. Pendekatan ini memungkinkan Anda untuk memutus koneksi antara implementasi tertentu dan menulis logika bisnis di plugin dan pustaka.


Dalam implementasi kanonik, setiap langkah menerima instance sendiri dari objek yang mengimplementasikan AsyncSteps , yang memungkinkan pelacakan tepat waktu kesalahan logis dalam menggunakan antarmuka.


Contoh abstrak:


  asi.add( // Level 0 step 1 func( asi ){ print( "Level 0 func" ) asi.add( // Level 1 step 1 func( asi ){ print( "Level 1 func" ) asi.error( "MyError" ) }, onerror( asi, error ){ // Level 1 step 1 catch print( "Level 1 onerror: " + error ) asi.error( "NewError" ) } ) }, onerror( asi, error ){ // Level 0 step 1 catch print( "Level 0 onerror: " + error ) if ( error strequal "NewError" ) { asi.success( "Prm", 123, [1, 2, 3], true) } } ) asi.add( // Level 0 step 2 func( asi, str_param, int_param, array_param ){ print( "Level 0 func2: " + param ) } ) 

Hasil Eksekusi:


  Level 0 func 1 Level 1 func 1 Level 1 onerror 1: MyError Level 0 onerror 1: NewError Level 0 func 2: Prm 

Dalam sinkronisasi, akan terlihat seperti ini:


  str_res, int_res, array_res, bool_res // undefined try { // Level 0 step 1 print( "Level 0 func 1" ) try { // Level 1 step 1 print( "Level 1 func 1" ) throw "MyError" } catch( error ){ // Level 1 step 1 catch print( "Level 1 onerror 1: " + error ) throw "NewError" } } catch( error ){ // Level 0 step 1 catch print( "Level 0 onerror 1: " + error ) if ( error strequal "NewError" ) { str_res = "Prm" int_res = 123 array_res = [1, 2, 3] bool_res = true } else { re-throw } } { // Level 0 step 2 print( "Level 0 func 2: " + str_res ) } 

Mimikri maksimum dari kode sinkron tradisional segera terlihat, yang seharusnya membantu keterbacaan.


Dari sudut pandang logika bisnis, sejumlah besar persyaratan tumbuh seiring waktu, tetapi kita dapat membaginya menjadi bagian-bagian yang mudah dipahami. Dijelaskan di bawah ini, hasil lari dalam praktik selama empat tahun.


API Core Runtime:


  1. add(func[, onerror]) - imitasi try-catch .
  2. success([args...]) - indikasi eksplisit penyelesaian yang berhasil:
    • tersirat secara default
    • dapat meneruskan hasilnya ke langkah berikutnya.
  3. error(code[, reason) - gangguan eksekusi dengan kesalahan:
    • code - memiliki tipe string yang lebih baik diintegrasikan dengan protokol jaringan dalam arsitektur layanan mikro,
    • reason - penjelasan sewenang-wenang bagi seseorang.
  4. state() - analog dari Penyimpanan Lokal Utas. Kunci asosiatif yang telah ditentukan:
    • error_info - penjelasan tentang kesalahan terakhir untuk seseorang,
    • last_exception - penunjuk ke objek pengecualian terakhir,
    • async_stack - setumpuk panggilan asinkron async_stack teknologi memungkinkan,
    • sisanya diatur oleh pengguna.

Contoh sebelumnya sudah dengan kode C ++ nyata dan beberapa fitur tambahan:


 #include <futoin/iasyncsteps.hpp> using namespace futoin; void some_api(IAsyncSteps& asi) { asi.add( [](IAsyncSteps& asi) { std::cout << "Level 0 func 1" << std::endl; asi.add( [](IAsyncSteps& asi) { std::cout << "Level 1 func 1" << std::endl; asi.error("MyError"); }, [](IAsyncSteps& asi, ErrorCode code) { std::cout << "Level 1 onerror 1: " << code << std::endl; asi.error("NewError", "Human-readable description"); } ); }, [](IAsyncSteps& asi, ErrorCode code) { std::cout << "Level 0 onerror 1: " << code << std::endl; if (code == "NewError") { // Human-readable error info assert(asi.state().error_info == "Human-readable description"); // Last exception thrown is also available in state std::exception_ptr e = asi.state().last_exception; // NOTE: smart conversion of "const char*" asi.success("Prm", 123, std::vector<int>({1, 2, 3}, true)); } } ); asi.add( [](IAsyncSteps& asi, const futoin::string& str_res, int int_res, std::vector<int>&& arr_res) { std::cout << "Level 0 func 2: " << str_res << std::endl; } ); } 

API untuk membuat loop:


  1. loop( func, [, label] ) - langkah dengan tubuh yang berulang berulang.
  2. forEach( map|list, func [, label] ) - langkah-iterasi dari objek koleksi.
  3. repeat( count, func [, label] ) - langkah-iterasi yang ditentukan berapa kali.
  4. break( [label] ) adalah analog dari interupsi loop tradisional.
  5. continue( [label] ) adalah analog dari kelanjutan loop tradisional dengan iterasi baru.

Spesifikasi ini menawarkan nama-nama alternatif breakLoop , continueLoop dan lainnya jika ada konflik dengan kata-kata yang dipesan.


Contoh C ++:


  asi.loop([](IAsyncSteps& asi) { // infinite loop asi.breakLoop(); }); asi.repeat(10, [](IAsyncSteps& asi, size_t i) { // range loop from i=0 till i=9 (inclusive) asi.continueLoop(); }); asi.forEach( std::vector<int>{1, 2, 3}, [](IAsyncSteps& asi, size_t i, int v) { // Iteration of vector-like and list-like objects }); asi.forEach( std::list<futoin::string>{"1", "2", "3"}, [](IAsyncSteps& asi, size_t i, const futoin::string& v) { // Iteration of vector-like and list-like objects }); asi.forEach( std::map<futoin::string, futoin::string>(), [](IAsyncSteps& asi, const futoin::string& key, const futoin::string& v) { // Iteration of map-like objects }); std::map<std::string, futoin::string> non_const_map; asi.forEach( non_const_map, [](IAsyncSteps& asi, const std::string& key, futoin::string& v) { // Iteration of map-like objects, note the value reference type }); 

API untuk integrasi dengan acara eksternal:


  1. setTimeout( timeout_ms ) - melempar kesalahan Timeout setelah timeout jika langkah dan subtree-nya belum menyelesaikan eksekusi.
  2. setCancel( handler ) - mengatur cancel handler, yang dipanggil ketika utas dibatalkan sama sekali dan ketika tumpukan langkah asinkron diperluas selama pemrosesan kesalahan.
  3. waitExternal() - tunggu sederhana untuk acara eksternal.
    • Catatan: Aman untuk digunakan hanya dalam teknologi dengan pengumpul sampah.

Panggilan ke salah satu dari fungsi-fungsi ini membuat panggilan eksplisit untuk success() diperlukan.


Contoh C ++:


  asi.add([](IAsyncSteps& asi) { auto handle = schedule_external_callback([&](bool err) { if (err) { try { asi.error("ExternalError"); } catch (...) { // pass } } else { asi.success(); } }); asi.setCancel([=](IAsyncSteps& asi) { external_cancel(handle); }); }); asi.add( [](IAsyncSteps& asi) { // Raises Timeout error after specified period asi.setTimeout(std::chrono::seconds{10}); asi.loop([](IAsyncSteps& asi) { // infinite loop }); }, [](IAsyncSteps& asi, ErrorCode code) { if (code == futoin::errors::Timeout) { asi(); } }); 

Contoh ECMAScript:


 asi.add( (asi) => { asi.waitExternal(); // disable implicit success() some_obj.read( (err, data) => { if (!asi.state) { // ignore as AsyncSteps execution got canceled } else if (err) { try { asi.error( 'IOError', err ); } catch (_) { // ignore error thrown as there are no // AsyncSteps frames on stack. } } else { asi.success( data ); } } ); } ); 

API Integrasi Masa Depan / Janji:


  1. await(promise_future[, on_error]) - menunggu Masa Depan / Janji sebagai langkah.
  2. promise() - mengubah seluruh aliran eksekusi ke Masa Depan / Janji, digunakan alih-alih execute() .

Contoh C ++:


  [](IAsyncSteps& asi) { // Proper way to create new AsyncSteps instances // without hard dependency on implementation. auto new_steps = asi.newInstance(); new_steps->add([](IAsyncSteps& asi) {}); // Can be called outside of AsyncSteps event loop // new_steps.promise().wait(); // or // new_steps.promise<int>().get(); // Proper way to wait for standard std::future asi.await(new_steps->promise()); // Ensure instance lifetime asi.state()["some_obj"] = std::move(new_steps); }; 

API Kontrol Alur Logika Bisnis:


  1. AsyncSteps(AsyncTool&) adalah konstruktor yang mengikat utas eksekusi ke Loop Acara tertentu.
  2. execute() - memulai utas eksekusi.
  3. cancel() - membatalkan utas eksekusi.

Implementasi antarmuka spesifik sudah diperlukan di sini.


Contoh C ++:


 #include <futoin/ri/asyncsteps.hpp> #include <futoin/ri/asynctool.hpp> void example() { futoin::ri::AsyncTool at; futoin::ri::AsyncSteps asi{at}; asi.loop([&](futoin::IAsyncSteps &asi){ // Some infinite loop logic }); asi.execute(); std::this_thread::sleep_for(std::chrono::seconds{10}); asi.cancel(); // called in d-tor by fact } 

API lain:


  1. newInstance() - memungkinkan Anda membuat utas eksekusi baru tanpa ketergantungan langsung pada implementasinya.
  2. sync(object, func, onerror) - sama, tetapi dengan sinkronisasi relatif terhadap objek yang mengimplementasikan antarmuka yang sesuai.
  3. parallel([on_error]) - add() khusus add() , subteps di antaranya adalah aliran AsyncSteps yang terpisah:
    • semua utas memiliki state() umum state() ,
    • utas induk melanjutkan eksekusi setelah semua anak selesai
    • kesalahan tanpa tertangkap pada anak mana pun segera membatalkan semua utas anak lainnya.

Contoh C ++:


  #include <futoin/ri/mutex.hpp> using namespace futoin; ri::Mutex mtx_a; void sync_example(IAsyncSteps& asi) { asi.sync(mtx_a, [](IAsyncSteps& asi) { // synchronized section asi.add([](IAsyncSteps& asi) { // inner step in the section // This synchronization is NOOP for already // acquired Mutex. asi.sync(mtx_a, [](IAsyncSteps& asi) { }); }); }); } void parallel_example(IAsyncSteps& asi) { using OrderVector = std::vector<int>; asi.state("order", OrderVector{}); auto& p = asi.parallel([](IAsyncSteps& asi, ErrorCode) { // Overall error handler asi.success(); }); p.add([](IAsyncSteps& asi) { // regular flow asi.state<OrderVector>("order").push_back(1); asi.add([](IAsyncSteps& asi) { asi.state<OrderVector>("order").push_back(4); }); }); p.add([](IAsyncSteps& asi) { asi.state<OrderVector>("order").push_back(2); asi.add([](IAsyncSteps& asi) { asi.state<OrderVector>("order").push_back(5); asi.error("SomeError"); }); }); p.add([](IAsyncSteps& asi) { asi.state<OrderVector>("order").push_back(3); asi.add([](IAsyncSteps& asi) { asi.state<OrderVector>("order").push_back(6); }); }); asi.add([](IAsyncSteps& asi) { asi.state<OrderVector>("order"); // 1, 2, 3, 4, 5 }); }; 

Primitif standar untuk sinkronisasi


  1. Mutex - membatasi eksekusi simultan N thread dengan antrian di Q , secara default N=1, Q=unlimited .
  2. Throttle - membatasi jumlah input N dalam periode P dengan antrian di Q , secara default N=1, P=1s, Q=0 .
  3. Limiter adalah kombinasi dari Mutex dan Throttle , yang biasanya digunakan pada input pemrosesan permintaan eksternal dan ketika memanggil sistem eksternal untuk tujuan operasi yang stabil di bawah beban.

Dalam hal DefenseRejected batas antrian, kesalahan DefenseRejected , artinya jelas dari deskripsi Limiter .


Manfaat Utama


Konsep AsyncSteps bukanlah tujuan itu sendiri, tetapi lahir dari kebutuhan untuk eksekusi program asinkron yang lebih terkontrol dalam hal batas waktu, pembatalan dan konektivitas keseluruhan dari callback individu. Tidak ada solusi universal pada waktu itu dan sekarang menyediakan fungsionalitas yang sama. Oleh karena itu:


FTN12 β€” .


setCancel() β€” . , . RAII atexit() .


cancel() β€” , . SIGTERM pthread_cancel() , .


setTimeout() β€” . , "Timeout".


β€” FutoIn AsyncSteps .


β€” ABI , . Embedded MMU.







Intel Xeon E3-1245v2/DDR1333 Debian Stretch .


:


  1. Boost.Fiber protected_fixedsize_stack .
  2. Boost.Fiber pooled_fixedsize_stack .
  3. FutoIn AsyncSteps .
  4. FutoIn AsyncSteps ( FUTOIN_USE_MEMPOOL=false ).
    • futoin::IMemPool .
  5. FutoIn NitroSteps<> β€” .
    • .

Boost.Fiber :


  1. 1 . .
  2. 30 . 1 . .
    • 30 . mmap()/mprotect() boost::fiber::protected_fixedsize_stack .
    • .
  3. 30 . 10 . .
    • "" .

"" , .. , . . .


GCC 6.3.0. lang tcmalloc , .


GitHub GitLab .


1.


Boost.Fiber protected4.8s208333.333Hz
Boost.Fiber pooled0.23s4347826.086Hz
FutoIn AsyncSteps0.21s4761904.761Hz
FutoIn AsyncSteps no mempool0.31s3225806.451Hz
FutoIn NitroSteps0.255s3921568.627Hz


β€” .


Boost.Fiber - , pooled_fixedsize_stack , AsyncSteps.


2.


Boost.Fiber protected6.31s158478.605Hz
Boost.Fiber pooled1.558s641848.523Hz
FutoIn AsyncSteps1.13s884955.752Hz
FutoIn AsyncSteps no mempool1.353s739098.300Hz
FutoIn NitroSteps1.43s699300.699Hz


β€” .


, . , β€” .


3.


Boost.Fiber protected5.096s1962323.390Hz
Boost.Fiber pooled5.077s1969667.126Hz
FutoIn AsyncSteps5.361s1865323.633Hz
FutoIn AsyncSteps no mempool8.288s1206563.706Hz
FutoIn NitroSteps3.68s2717391.304Hz


β€” .


, Boost.Fiber AsyncSteps, NitroSteps.


( RSS)


Boost.Fiber protected124M
Boost.Fiber pooled505M
FutoIn AsyncSteps124M
FutoIn AsyncSteps no mempool84M
FutoIn NitroSteps115M


β€” .


, Boost.Fiber .


: Node.js


- Promise : + 10 . . 10 . JIT NODE_ENV=production , @futoin/optihelp .


GitHub GitLab . Node.js v8.12.0 v10.11.0, FutoIn CID .


TechSimpleLoop
Node.js v10
FutoIn AsyncSteps1342899.520Hz587.777Hz
async/await524983.234Hz630.863Hz
Node.js v8
FutoIn AsyncSteps682420.735Hz588.336Hz
async/await365050.395Hz400.575Hz



β€” .


async/await ? , V8 Node.js v10 .


, Promise async/await Node.js Event Loop. ( ), FutoIn AsyncSteps .


AsyncSteps Node.js Event Loop async/await - Node.js v10.


, ++ β€” . , Node.js 10 .


Kesimpulan


C++, FutoIn AsyncSteps Boost.Fiber , Boost.Fiber mmap()/mprotect .


, - , . .


FutoIn AsyncSteps JavaScript async/await Node.js v10.


, -, . .


- "" . β€” API.




Kesimpulan


, FutoIn AsyncSteps , "" async/await . , . Promise ECMAScript, AsyncSteps "" .


. AsyncSteps NitroSteps .


, - .


Java/JVM β€” . .


, GitHub / GitLab .

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


All Articles