Di bidang pengembangan aplikasi multithreaded atau terdistribusi sangat tinggi, diskusi tentang pemrograman asinkron sering muncul. Hari ini kita akan menyelami asynchrony secara terperinci dan mempelajari apa itu ketika itu terjadi, bagaimana hal itu mempengaruhi kode dan bahasa pemrograman yang kita gunakan. Kami akan mencari tahu mengapa Berjangka dan Janji diperlukan, dan menyentuh coroutine dan sistem operasi. Ini akan membuat pertukaran yang muncul selama pengembangan perangkat lunak lebih eksplisit.
Materi tersebut didasarkan pada transkrip laporan oleh Ivan Puzyrevsky, seorang guru di Yandex Data Analysis School.

Rekaman video
1. Konten
2. Pendahuluan
Halo semuanya, nama saya Ivan Puzyrevsky, saya bekerja untuk Yandex. Selama enam tahun terakhir saya telah terlibat dalam infrastruktur penyimpanan dan pemrosesan data, sekarang saya beralih ke produk - untuk mencari perjalanan, hotel, dan tiket. Karena saya sudah lama bekerja di infrastruktur, saya telah memperoleh cukup banyak pengalaman tentang cara menulis berbagai aplikasi yang dimuat. Infrastruktur kami beroperasi 24*7*365
setiap hari tanpa henti, terus menerus di ribuan mesin. Secara alami, Anda perlu menulis kode agar dapat bekerja dengan andal dan efisien dan menyelesaikan tugas yang dipikul perusahaan.
Hari ini kita akan berbicara tentang asinkron. Apa itu asinkron? Ini adalah ketidakcocokan sesuatu dengan sesuatu pada waktunya. Dari uraian ini, umumnya tidak jelas apa yang akan saya bicarakan hari ini. Untuk memperjelas masalah ini, saya perlu contoh a la "Halo, dunia!". Asynchrony biasanya terjadi dalam konteks penulisan aplikasi jaringan, jadi saya akan memiliki analog jaringan "Halo, dunia!". Ini adalah aplikasi ping-pong. Kode ini terlihat seperti ini:
socket s; string x; x = read_from_socket(s, 4); if (x == "ping") { write_to_socket(s, "pong"); } return;
Saya membuat soket, membaca garis dari sana, dan memeriksa apakah itu ping, maka saya menulis pong sebagai tanggapan. Sangat sederhana dan jelas. Apa yang terjadi ketika Anda melihat kode seperti itu di layar komputer Anda? Kami menganggap kode ini sebagai urutan langkah-langkah ini:

Dari sudut pandang waktu fisik nyata, semuanya agak bias.

Mereka yang benar-benar menulis dan menjalankan kode seperti itu tahu bahwa setelah langkah membaca dan setelah langkah
tulis adalah interval waktu yang cukup mencolok ketika program kita tampaknya tidak melakukan apa-apa dari sudut pandang kode kita, tetapi di bawah kap mesin beroperasi, yang kita sebut "input-output".

Selama I / O, paket dipertukarkan melalui jaringan dan semua petugas berat, pekerjaan tingkat rendah. Mari kita lakukan eksperimen pemikiran: ambil satu program seperti itu, jalankan pada satu prosesor fisik dan berpura-pura bahwa kita tidak memiliki sistem operasi, apa yang akan terjadi? Prosesor tidak dapat berhenti, ia terus melakukan langkah-langkah tanpa mengikuti instruksi apa pun, hanya membuang-buang energi dengan sia-sia.

Muncul pertanyaan apakah kita dapat melakukan sesuatu yang bermanfaat selama periode waktu ini. Ini adalah pertanyaan yang sangat alami, jawaban yang memungkinkan kita menghemat daya prosesor dan menggunakannya untuk sesuatu yang bermanfaat, sementara aplikasi kita tampaknya tidak melakukan apa-apa.
3. Konsep dasar
3.1. Utas eksekusi
Bagaimana kita bisa mendekati tugas ini? Mari kita rujuk konsep-konsepnya. Saya akan mengatakan "aliran eksekusi", mengacu pada beberapa urutan yang berarti dari operasi atau langkah-langkah dasar. Makna akan ditentukan oleh konteks di mana saya berbicara tentang aliran eksekusi. Artinya, jika kita berbicara tentang algoritma single-threaded (Aho-Korasik, pencarian grafik), maka algoritma ini sendiri sudah menjadi utas eksekusi. Dia mengambil beberapa langkah untuk menyelesaikan masalah.
Jika saya berbicara tentang database, maka satu utas eksekusi dapat menjadi bagian dari tindakan yang dilakukan oleh database untuk melayani satu permintaan masuk. Hal yang sama berlaku untuk server web. Jika saya menulis beberapa jenis aplikasi seluler atau web, maka untuk melayani operasi satu pengguna, misalnya, mengklik tombol, interaksi jaringan, interaksi dengan penyimpanan lokal, dan sebagainya. Urutan tindakan ini dari sudut pandang aplikasi seluler saya juga akan menjadi alur eksekusi yang bermakna dan terpisah. Dari sudut pandang sistem operasi, proses atau utas proses juga merupakan untaian eksekusi yang bermakna.
3.2. Multitasking dan konkurensi
Landasan produktivitas adalah kemampuan untuk melakukan trik seperti itu: ketika saya memiliki satu utas eksekusi yang berisi rongga dalam pemindaian waktu fisiknya, lalu isi rongga ini dengan sesuatu yang bermanfaat - ikuti langkah-langkah dari rangkaian eksekusi lainnya.

Database biasanya melayani banyak klien secara bersamaan. Jika kita dapat menggabungkan kerja pada beberapa utas eksekusi dalam kerangka satu utas eksekusi tingkat yang lebih tinggi, maka ini disebut multitasking. Yaitu, multitasking adalah ketika saya melakukan tindakan dalam kerangka satu alur eksekusi yang lebih besar yang lebih rendah dari solusi tugas-tugas kecil.
Penting untuk tidak membingungkan konsep multitasking dengan paralelisme. Concurrency -
ini adalah properti dari lingkungan runtime, yang memungkinkan dalam satu langkah waktu, dalam satu langkah, untuk membuat kemajuan dalam utas eksekusi yang berbeda. Jika saya memiliki dua prosesor fisik, maka dalam satu siklus clock mereka dapat menjalankan dua instruksi. Jika program berjalan pada satu prosesor, maka akan dibutuhkan dua siklus clock untuk menjalankan dua instruksi yang sama.

Penting untuk tidak membingungkan konsep-konsep ini, karena mereka masuk dalam kategori yang berbeda. Multitasking adalah fitur program Anda yang terstruktur secara internal sebagai variabel yang bekerja pada tugas yang berbeda. Concurrency adalah properti dari lingkungan runtime yang memungkinkan Anda untuk mengerjakan beberapa tugas dalam satu siklus clock.
Dalam banyak hal, kode asinkron dan penulisan kode asinkron adalah menulis kode multitasking. Kesulitan utama adalah bagaimana saya menyandikan tugas dan cara mengelolanya. Karena itu, hari ini kita akan membicarakan ini - menulis kode multitasking.
4. Memblokir dan menunggu

Mari kita mulai dengan beberapa contoh sederhana. Kembali ke ping-pong:
socket s; string x; x = read_from_socket(s, 4); if (x == "ping") { write_to_socket(s, "pong"); } return;
Seperti yang telah kita bahas, setelah membaca dan garis putih utas eksekusi tertidur, itu diblokir. Biasanya kita berkata, "aliran diblokir."
socket s; string x; x = read_from_socket(s, 4); if (x == "ping") { write_to_socket(s, "pong"); } return;
Ini berarti bahwa alur eksekusi telah mencapai titik di mana setiap peristiwa diperlukan untuk melanjutkannya. Khususnya, dalam hal aplikasi jaringan kami, perlu bahwa data tiba melalui jaringan atau, sebaliknya, kami memiliki buffer gratis untuk menulis data ke jaringan. Peristiwa mungkin berbeda. Jika kita berbicara tentang aspek waktu, kita bisa menunggu timer untuk memicu atau menyelesaikan proses lain. Peristiwa di sini adalah semacam hal yang abstrak, tentang hal itu penting untuk dipahami bahwa mereka dapat diharapkan.

Ketika kami menulis kode sederhana, kami secara implisit memberikan kendali atas ekspektasi acara ke tingkat yang lebih tinggi. Dalam kasus kami, sistem operasi. Dia, sebagai entitas tingkat yang lebih tinggi, bertanggung jawab untuk memilih tugas mana yang akan dilakukan selanjutnya, dan dia juga bertanggung jawab untuk melacak terjadinya peristiwa.
Kode kami, yang kami tulis sebagai pengembang, disusun pada saat yang sama mengenai pekerjaan pada satu tugas. Cuplikan kode dari contoh menangani satu koneksi: ia membaca ping dari satu koneksi dan menulis pong ke satu koneksi.
Kode ini jelas. Anda dapat membacanya dan memahami apa fungsinya, cara kerjanya, masalah apa yang dipecahkan, invarian apa yang dimilikinya, dan sebagainya. Pada saat yang sama, kami mengelola perencanaan tugas dengan sangat buruk dalam model seperti itu. Secara umum, sistem operasi memiliki konsep prioritas, tetapi jika Anda menulis sistem waktu nyata yang lunak, maka Anda tahu bahwa alat yang tersedia di Linux tidak cukup untuk membuat sistem waktu nyata yang cukup waras.
Lebih jauh, sistem operasi adalah hal yang rumit, dan mengalihkan konteks dari aplikasi kita ke kernel menghabiskan beberapa mikrodetik, yang, dengan beberapa perhitungan sederhana, memberi kita perkiraan sekitar 20-100 ribu konteks switch per detik. Ini berarti bahwa jika kita menulis server web, maka dalam satu detik kita dapat memproses sekitar 20 ribu permintaan, dengan asumsi bahwa memproses permintaan sepuluh kali lebih mahal daripada sistem.

4.1. Menunggu tanpa pemblokiran

Jika Anda datang ke situasi yang Anda butuhkan untuk bekerja dengan jaringan lebih efisien, maka Anda mulai mencari bantuan di Internet dan datang untuk menggunakan select / epoll. Di Internet tertulis bahwa jika Anda ingin melayani ribuan koneksi pada saat yang sama, Anda perlu epoll, karena ini adalah mekanisme yang baik dan seterusnya. Anda membuka dokumentasi dan melihat sesuatu seperti ini:
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout); void FD_CLR(int fd, fd_set* set); int FD_ISSET(int fd, fd_set* set); void FD_SET(int fd, fd_set* set); void FD_ZERO(fd_set* set); int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event); int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
Fungsi di mana antarmuka berisi banyak deskriptor yang bekerja dengan Anda (dalam kasus pilih), atau banyak peristiwa yang berlalu
melintasi batas aplikasi Anda, kernel dari sistem operasi yang perlu Anda proses (dalam kasus epoll).
Perlu juga ditambahkan bahwa Anda bisa datang bukan untuk memilih / epoll, tetapi ke perpustakaan seperti libuv, yang tidak akan memiliki acara di API, tetapi akan memiliki banyak panggilan balik. Antarmuka perpustakaan akan mengatakan: "Dear friend, sediakan panggilan balik untuk membaca soket, yang akan saya panggil saat datanya muncul."
int uv_timer_start(uv_timer_t* handle, uv_timer_cb cb, uint64_t timeout, uint64_t repeat); typedef void (*uv_timer_cb)(uv_timer_t* handle); int uv_read_start(uv_stream_t* stream, uv_alloc_cb alloc_cb, uv_read_cb read_cb); int uv_read_stop(uv_stream_t*); typedef void (*uv_read_cb)(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf); int uv_write(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, uv_write_cb cb); typedef void (*uv_write_cb)(uv_write_t* req, int status);
Apa yang telah berubah dibandingkan dengan kode sinkron kami di bab sebelumnya? Kode telah menjadi tidak sinkron. Ini berarti bahwa kami mengambil logika ke dalam aplikasi untuk menentukan titik waktu ketika peristiwa dipantau. Panggilan pilih / epoll eksplisit adalah titik di mana kami meminta sistem operasi untuk informasi tentang peristiwa yang telah terjadi. Kami juga mengambil ke dalam kode aplikasi kami pilihan tugas mana yang harus dikerjakan selanjutnya.

Dari contoh antarmuka, Anda dapat melihat bahwa pada dasarnya ada dua mekanisme untuk memperkenalkan multitasking. Satu jenis "tarik" ketika kita
kita menarik banyak peristiwa yang kita tunggu, dan kemudian kita entah bagaimana bereaksi terhadapnya. Dalam pendekatan ini, mudah untuk mengamortisasi overhead oleh satu
suatu peristiwa dan karenanya mencapai throughput tinggi dalam komunikasi tentang serangkaian peristiwa yang telah terjadi. Biasanya, semua elemen jaringan seperti interaksi kernel dengan kartu jaringan atau interaksi Anda dan sistem operasi dibangun berdasarkan mekanisme polling.
Cara kedua adalah mekanisme "push", ketika entitas eksternal tertentu dengan jelas masuk, mengganggu aliran eksekusi dan mengatakan: "Sekarang, tolong tangani kejadian yang baru saja tiba." Ini adalah pendekatan dengan callback, dengan sinyal unix, dengan gangguan di tingkat prosesor, ketika entitas eksternal dengan jelas menginvasi utas eksekusi Anda dan berkata: "Sekarang, tolong, kami sedang mengerjakan acara ini." Pendekatan ini telah muncul untuk mengurangi penundaan antara terjadinya suatu peristiwa dan reaksi terhadapnya.
Mengapa kami pengembang C ++ yang menulis dan memecahkan masalah aplikasi tertentu mungkin ingin menyeret model acara ke dalam kode kami? Jika kita seret dan lepas pekerjaan pada banyak tugas ke dalam kode kita dan mengelolanya, maka karena kurangnya transisi ke kernel dan sebaliknya, kita dapat bekerja sedikit lebih cepat dan melakukan tindakan yang lebih bermanfaat per unit waktu.
Apa yang menyebabkan hal ini dalam hal kode yang kita tulis? Ambil nginx, misalnya, server HTTP berkinerja tinggi, sangat umum. Jika Anda membaca kodenya, kode ini dibuat berdasarkan model asinkron. Kode ini cukup sulit dibaca. Ketika Anda bertanya pada diri sendiri apa sebenarnya yang terjadi ketika memproses satu permintaan HTTP, ternyata kode tersebut memiliki banyak fragmen yang ditempatkan dalam file yang berbeda, pada sudut yang berbeda dari basis kode. Setiap fragmen melakukan sejumlah kecil pekerjaan sebagai bagian dari melayani seluruh permintaan HTTP. Sebagai contoh:
static void ngx_http_request_handler(ngx_event_t *ev) { … if (c->close) { ngx_http_terminate_request(r, 0); return; } if (ev->write) { r->write_event_handler(r); } else { r->read_event_handler(r); } ... } typedef void (*ngx_http_event_handler_pt)(ngx_http_request_t *r); struct ngx_http_request_s { ngx_http_event_handler_pt read_event_handler; }; r->read_event_handler = ngx_http_request_empty_handler; r->read_event_handler = ngx_http_block_reading; r->read_event_handler = ngx_http_test_reading; r->read_event_handler = ngx_http_discarded_request_body_handler; r->read_event_handler = ngx_http_read_client_request_body_handler; r->read_event_handler = ngx_http_upstream_rd_check_broken_connection; r->read_event_handler = ngx_http_upstream_read_request_handler;
Ada struktur permintaan, yang diteruskan ke pengendali acara ketika sinyal soket akses baca atau tulis. Selanjutnya, pawang ini terus-menerus beralih dalam program tergantung pada kondisi pemrosesan permintaan. Entah kita membaca tajuk, atau membaca badan permintaan, atau meminta data hulu - secara umum, ada banyak kondisi berbeda.
Kode semacam itu sulit dibaca karena pada dasarnya dijelaskan dalam bentuk reaksi terhadap peristiwa. Kita berada dalam keadaan ini dan itu dan bereaksi dengan cara tertentu terhadap peristiwa yang telah terjadi. Tidak ada gambaran lengkap tentang seluruh proses pemrosesan permintaan HTTP.
Pilihan lain, yang sering digunakan dalam JavaScript, adalah untuk membangun kode berbasis panggilan balik ketika kita meneruskan panggilan balik kita ke panggilan antarmuka, di mana biasanya ada beberapa panggilan balik bersarang untuk suatu acara, dan sebagainya.
int LibuvStreamWrap::ReadStart() { return uv_read_start(stream(), [](uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { static_cast<LibuvStreamWrap*>(handle->data)->OnUvAlloc(suggested_size, buf); }, [](uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { static_cast<LibuvStreamWrap*>(stream->data)->OnUvRead(nread, buf); }); } for (p=data; p != data + len; p++) { ch = *p; reexecute: switch (CURRENT_STATE()) { case s_start_req_or_res: case s_res_or_resp_H: case s_res_HT: case s_res_HTT: case s_res_HTTP: case s_res_http_major: case s_res_http_dot:
Sekali lagi kode ini sangat terfragmentasi, tidak ada pemahaman tentang kondisi saat ini tentang bagaimana kami bekerja berdasarkan permintaan. Banyak informasi yang dikirimkan melalui penutupan, dan Anda perlu melakukan upaya mental untuk merekonstruksi logika memproses satu permintaan.
Dengan demikian, memperkenalkan multitasking ke dalam kode kami (logika memilih tugas kerja dan multiplexing), kami mendapatkan kode yang efektif dan kontrol atas prioritas tugas, tetapi kami kehilangan banyak tugas karena mudah dibaca. Kode ini sulit dibaca dan sulit dipertahankan.

Mengapa Misalkan saya punya kasus sederhana, misalnya, saya membaca file dan mentransfernya melalui jaringan. Dalam versi non-pemblokiran, kasing ini akan sesuai dengan mesin keadaan linier tersebut:
- Keadaan awal
- Mulai membaca file,
- Menunggu respons dari sistem file,
- Menulis file ke soket,
- Keadaan akhir.
Sekarang, katakanlah saya ingin menambahkan informasi dari database ke file ini. Opsi sederhana:
- keadaan awal
- membaca file
- baca file
- membaca dari database
- baca dari database,
- Saya bekerja dengan soket
- menulis ke soket.
Sepertinya kode linier, tetapi jumlah negara telah meningkat.
Kemudian Anda mulai berpikir bahwa akan lebih baik untuk memparalelkan dua langkah - membaca dari file dan dari database. Keajaiban kombinatorik dimulai: Anda berada dalam keadaan awal, meminta untuk membaca file dan data dari database. Kemudian Anda bisa datang ke keadaan di mana ada data dari database, tetapi tidak ada file, atau sebaliknya - ada data dari file, tetapi tidak dari database. Selanjutnya, Anda harus pergi ke keadaan di mana Anda memiliki satu dari dua hal. Sekali lagi, ini adalah dua kondisi. Maka Anda harus pergi ke keadaan di mana Anda memiliki kedua bahan tersebut. Kemudian tuliskan ke soket dan sebagainya.
Semakin kompleks aplikasi, semakin banyak status, semakin banyak fragmen kode yang perlu digabungkan di kepala Anda. Nyaman Atau Anda menulis mie panggilan balik, yang tidak nyaman untuk dibaca. Jika sistem percabangan ditulis, maka suatu hari akan tiba saatnya Anda tidak bisa lagi menerimanya.
5. Berjangka / Janji

Untuk mengatasi masalah, Anda perlu melihat situasi dengan lebih mudah.

Ada sebuah program, ia memiliki lingkaran hitam dan merah. Alur eksekusi kami adalah lingkaran hitam; terkadang mereka diselingi dengan warna merah ketika aliran tidak dapat melanjutkan pekerjaannya. Masalahnya adalah bahwa untuk eksekusi thread hitam kami Anda harus masuk ke lingkaran hitam berikutnya, yang tidak akan diketahui kapan.
Masalahnya adalah ketika kita menulis kode dalam bahasa pemrograman, kita menjelaskan kepada komputer apa yang harus dilakukan sekarang. Komputer adalah hal yang relatif sederhana yang mengharapkan instruksi yang kita tulis dalam bahasa pemrograman. Dia sedang menunggu instruksi untuk lingkaran berikutnya, dan dalam bahasa pemrograman kami tidak ada cukup uang untuk mengatakan: "Di masa depan, tolong, ketika sesuatu terjadi, lakukan sesuatu."

Dalam bahasa pemrograman, kami beroperasi dengan tindakan sesaat yang dapat dimengerti: memanggil fungsi, operasi aritmatika, dll. Mereka menggambarkan langkah selanjutnya yang spesifik berikutnya. Pada saat yang sama, untuk memproses logika aplikasi, perlu untuk menggambarkan bukan langkah fisik berikutnya, tetapi langkah logis berikutnya: apa yang harus kita lakukan ketika data dari database muncul, misalnya.

Oleh karena itu, kita memerlukan mekanisme bagaimana menggabungkan fragmen-fragmen ini. Dalam kasus ketika kami menulis kode sinkron, kami menyembunyikan pertanyaan sepenuhnya di bawah tenda dan mengatakan bahwa sistem operasi akan menanganinya, membiarkannya mengganggu dan menjadwal ulang utas kami.
Di level 1, kami membuka kotak Pandora ini, dan membawa banyak sakelar, kasing, kondisi, cabang, status ke kode. Saya ingin beberapa kompromi sehingga kodenya relatif mudah dibaca, tetapi tetap mempertahankan semua kelebihan level 1.
Untungnya bagi kami, pada tahun 1988 orang yang terlibat dalam sistem terdistribusi, Barbara Liskov dan Luba Shirir, menyadari masalahnya, dan sampai pada kebutuhan akan perubahan linguistik. Penting untuk menambahkan konstruksi ke bahasa pemrograman yang memungkinkan mengekspresikan hubungan temporal antara peristiwa - pada saat saat ini dalam waktu dan pada saat yang tidak pasti di masa depan.
Ini disebut Janji. Konsepnya keren, tapi sudah terkumpul di rak selama dua puluh tahun. — , Twitter, Ruby on Rails Scala, , , , future . Your Server as a Function. , .
Scala, , ++ ?
, Future. T c : , - .
template <class T> class Future <T>
, , , . , «», , . Future «», Promise — «». ; , JavaScript, Promise — , Java – Future.
, . , , boost::future ( std::future) — , .
5.1. Future & Promise
template <class T> class Future { bool IsSet() const; const T& Get() const; T* TryGet() const; void Subscribe(std::function<void(const T&)> cb); template <class R> Future<R> Then( std::function<R(const T&)> f); template <class R> Future<R> Then( std::function<Future<R>(const T&)> f); }; template <class T> Future<T> MakeFuture(const T& value);
, , - , . , , , . , , — , , . Then, .
template <class T> class Promise { bool IsSet() const; void Set(const T& value); bool TrySet(const T& value); Future<T> ToFuture() const; }; template <class T> Promise<T> NewPromise();
. , . «, , , ».
5.2.

? , . Then — , .
, — future --, - t — . , , , f, - r.
t f. , , r.
: t, , r . :
template <class T> template <class R> Future<R> Future<T>::Then(std::function<R(const T&)> f) { auto promise = NewPromise<R>(); this->Subscribe([promise] (const T& t) { auto r = f(t); promise.Set(r); }); return promise.ToFuture(); }
:
Promise
R
,Future<T>
t
,- ,
r = f(t)
, r
Promise
,Promise
.
f
, R
, Future<R>
, R
. :
template <class T> template <class R> Future<R> Future<T>::Then(std::function<Future<R>(const T&)> f) { auto promise = NewPromise<R>(); this->Subscribe([promise] (const T& t) { auto that = f(t); that.Subscribe([promise] (R r) { promise.Set(r); }); }); return promise.ToFuture(); }
, - t. f, r, . , , .

, Then :
Promise
,Subscribe
-,Promise
, Future
.
, . , , , .
, , , -. , , -, Subscribe. , , , - . , .
5.3. Contohnya
AsyncComputeValue, GPU, . Then, , (2v+1) 2 .
Future<int> value = AsyncComputeValue();
. , : (2v+1) 2 . , .
, , . .
. : , ; ; .
Future<int> GetDbKey(); Future<string> LoadDbValue(int key); Future<void> SendToMars(string message); Future<void> ExploreOuterSpace() { return GetDbKey()
— ExploreOuterSpace. Then; — — , . ( ) . .
5.4. Any-
: Future
, , . , , :
template <class T> Future<T> Any(Future<T> f1, Future<T> f2) { auto promise = NewPromise<T>(); f1.Subscribe([promise] (const T& t) { promise.TrySet(t); }); f2.Subscribe([promise] (const T& t) { promise.TrySet(t); }); return promise.ToFuture(); }
, Any-, Future : , . , , .
, , , , , . « DB1, DB2, — - ».
5.5. All-
. , , , ( T1 T2), T1 T2 , , .
template <class T1, class T2> Future<std::tuple<T1, T2>> All(Future<T1> f1, Future<T2> f2) { auto promise = NewPromise<std::tuple<T1, T2>>(); auto result = std::make_shared< std::tuple<T1, T2> >(); auto counter = std::make_shared< std::atomic<int> >(2); f1.Subscribe([promise, result, counter] (const T1& t1) { std::get<0>(*result) = t1; if (--(*counter) == 0) { promise.Set(*result)); } }); f2.Subscribe([promise, result, counter] (const T2& t2) { } return promise.ToFuture(); }
nginx. , , . nginx « », « », « » . All- , . .
5.6.
Future Promises — legacy-, . callback- , , : Future, , callback- Future.
: , Future .
6.

, , . .
Future<Request> GetRequest(); Future<Payload> QueryBackend(Request req); Future<Response> HandlePayload(Payload pld); Future<void> Reply(Request req, Response rsp);
. Request, - . , . , , , . , - .
, , . ? — , request payload, — , .
, Java Netty. , , . , , .
, GetRequest, QueryBackend, HandlePayload Reply , Future.
, , Future T — WaitFor.
Future<Request> GetRequest(); Future<Payload> QueryBackend(Request req); Future<Response> HandlePayload(Payload pld); Future<void> Reply(Request req, Response rsp); template <class T> T WaitFor(Future<T> future);
:
Future<Request> GetRequest(); Future<Payload> QueryBackend(Request req); Future<Response> HandlePayload(Payload pld); Future<void> Reply(Request req, Response rsp); template <class T> T WaitFor(Future<T> future); auto req = WaitFor(GetRequest()); auto pld = WaitFor(QueryBackend(req)); auto rsp = WaitFor(HandlePayload(pld)); WaitFor(Reply(req, rsp));
: Future, . . , . .
. . - 0, , , mutex+cvar future. . , .

6.1.
, . , , , , - , . , - .
— «» , , . . . : boost::asio boost::fiber.
, . Bagaimana cara melakukannya?
6.2. WaitFor
, , boost::context, : , ; , . x86/64 , , .
, goto: , , , .
, - . Fiber — . +Future. , , Future, .
class Fiber { MachineContext context_; Future<void> future_; };
class Scheduler { void WaitFor(Future<void> future); void Loop(); MachineContext loop_context_; Fiber* current_fiber_; std::deque<Fiber*> run_queue_; };
Future , , , . : Loop, , , , , .
WaitFor?
thread_local Scheduler* ThisScheduler; template <class T> T WaitFor(Future<T> future) { ThisScheduler->WaitFor(future.As<void>()); return future.Get(); } void Scheduler::WaitFor(Future<void> future) { current_fiber_->future_ = future; SwitchContext(¤t_fiber_->context_, &loop_context_); }
: , - , , Future void, . .
Future<void>
, , - .
WaitFor : : « Fiber Future», ( ) .
, :
ThisScheduler->WaitFor
return future.Get()
, .
? , Future, .
6.3.
- , , , - , . SwitchContext , 2 — .
void Scheduler::Loop() { while (true) {
? , , , Future, Future, , , .
void Scheduler::Loop() { while (true) {
, . :
WaitFor — .

Switch- .

Future ( ), , . - Fiber.

WaitFor Future , - , Future . :
Future<Request> GetRequest(); Future<Payload> QueryBackend(Request req); Future<Response> HandlePayload(Payload pld); Future<void> Reply(Request req, Response rsp); template <class T> T WaitFor(Future<T> future); auto req = WaitFor( GetRequest()); auto pld = WaitFor( QueryBackend(req)); auto rsp = WaitFor( HandlePayload(pld)); WaitFor( Reply(req, rsp));
, , , . , , .
6.4. Coroutine TS
? — . Coroutine TS, , WaitFor CoroutineWait, CoroutineTS — - . , - . , Waiter Co, , .
7. ?
. , , , . , , , .
— . , . . , . , , , , .
- , , . , . , , .

, ? , .
. , , , , . . , , , , .
nginx, , , , , . , , , future promises.
, , , , , , , .
futures, promises actors. . , .
: , , , . , , , , . ? , .
Menit periklanan. 19-20 C++ Russia 2019. , , Grimm Rainer «Concurrency and parallelism in C++17 and C++20/23» , C++ . , . , , - .