في مجال تطوير تطبيقات محمولة ذات مؤشرات ترابط متعددة أو موزعة ، غالبًا ما تنشأ مناقشات حول البرمجة غير المتزامنة. اليوم سوف نتعمق في عدم التزامن بالتفصيل ودراسة ما هو عليه عندما يحدث ، وكيف يؤثر على لغة البرمجة ولغة البرمجة التي نستخدمها. سنعرف سبب الحاجة إلى العقود المستقبلية والوعود ، وسنتطرق إلى أنظمة التشغيل وأنظمة التشغيل. هذا سيجعل المفاضلات التي تنشأ أثناء تطوير البرمجيات أكثر وضوحا.
تستند المادة إلى نسخة من تقرير أعده إيفان بوزيريفسكي ، وهو مدرس في مدرسة تحليل بيانات ياندكس.

تسجيل الفيديو
1. المحتوى
2. مقدمة
مرحباً بالجميع ، اسمي إيفان Puzyrevsky ، أعمل لدى Yandex. على مدار السنوات الست الماضية ، شاركت في البنية التحتية لتخزين ومعالجة البيانات ، والآن انتقلت إلى المنتج - بحثًا عن السفر والفنادق وتذاكر السفر. منذ أن عملت لفترة طويلة في البنية التحتية ، اكتسبت خبرة كبيرة في كيفية كتابة التطبيقات المحملة المختلفة. تعمل البنية الأساسية لدينا على 24*7*365
يوميًا طوال أيام الأسبوع دون توقف ، وبشكل مستمر على الآلاف من الأجهزة. بطبيعة الحال ، تحتاج إلى كتابة التعليمات البرمجية بحيث تعمل بشكل موثوق وفعال ويحل المهام التي تفرضها الشركة.
اليوم سنتحدث عن عدم التزامن. ما هو عدم التزامن؟ هو عدم تطابق شيء مع شيء في الوقت المناسب. من هذا الوصف ، من غير الواضح عمومًا ما سأتحدث عنه اليوم. لتوضيح القضية بطريقة أو بأخرى ، أحتاج إلى مثال "مرحباً يا عالم!". يحدث عدم التزامن عادةً في سياق كتابة تطبيقات الشبكة ، لذلك سيكون لديّ نظير شبكة "Hello، world!". هذا هو تطبيق كرة الطاولة. الرمز يشبه هذا:
socket s; string x; x = read_from_socket(s, 4); if (x == "ping") { write_to_socket(s, "pong"); } return;
أقوم بإنشاء مقبس ، وقراءة سطر من هناك ، وتحقق مما إذا كان بينغ ، ثم أكتب بونغ ردًا. بسيط جدا وواضح. ماذا يحدث عندما ترى هذا الرمز على شاشة الكمبيوتر الخاص بك؟ نفكر في هذا الرمز كتسلسل من هذه الخطوات:

من وجهة نظر الوقت الفعلي الفعلي ، كل شيء متحيز قليلاً.

أولئك الذين قاموا بالفعل بكتابة وتشغيل هذا الرمز يعرفون ذلك بعد قراءة الخطوة وبعد الخطوة
الكتابة هي فترة زمنية ملحوظة إلى حد ما عندما يبدو أن برنامجنا لا يفعل شيئًا من وجهة نظر كودنا ، ولكن تحت غطاء محرك السيارة تعمل الماكينة ، والتي نسميها "المدخلات والمخرجات".

أثناء الإدخال / الإخراج ، يتم تبادل الحزم عبر الشبكة وجميع الأعمال الثقيلة ذات المستوى المنخفض. دعنا نجري تجربة فكرية: خذ مثل هذا البرنامج ، وقم بتشغيله على معالج مادي واحد والتظاهر بأنه ليس لدينا أي نظام تشغيل ، ماذا سيحدث؟ لا يمكن للمعالج التوقف ، فهو مستمر في اتخاذ التدابير دون اتباع أي تعليمات ، فقط إهدار الطاقة دون جدوى.

السؤال الذي يطرح نفسه هو ما إذا كان بإمكاننا فعل أي شيء مفيد خلال هذه الفترة الزمنية. هذا سؤال طبيعي للغاية ، حيث تتيح لنا الإجابة توفير طاقة المعالج واستخدامه في شيء مفيد ، بينما يبدو أن تطبيقنا لا يفعل شيئًا.
3. المفاهيم الأساسية
3.1. موضوع التنفيذ
كيف يمكننا التعامل مع هذه المهمة؟ دعونا التوفيق بين المفاهيم. سأقول "تدفق التنفيذ" ، في إشارة إلى بعض التسلسل الهادف للعمليات الأولية أو الخطوات. سيتم تحديد المعنى من خلال السياق الذي أتحدث فيه عن تدفق التنفيذ. هذا هو ، إذا كنا نتحدث عن خوارزمية مفردة الترابط (Aho-Korasik ، البحث في الرسم البياني) ، فإن هذه الخوارزمية نفسها هي بالفعل سلسلة من التنفيذ. يأخذ بعض الخطوات لحل المشكلة.
إذا كنت أتحدث عن قاعدة بيانات ، فقد يكون مؤشر ترابط التنفيذ واحدًا جزءًا من الإجراءات التي تنفذها قاعدة البيانات لخدمة طلب وارد واحد. الشيء نفسه ينطبق على خوادم الويب. إذا كنت أكتب نوعًا من تطبيق الهاتف المحمول أو الويب ، فقم بخدمة أحد المستخدمين ، على سبيل المثال ، النقر فوق زر ، وتفاعلات الشبكة ، والتفاعل مع التخزين المحلي ، وما إلى ذلك. سيكون تسلسل هذه الإجراءات من وجهة نظر تطبيقي للهاتف المحمول أيضًا تدفقًا منفصلاً وهامًا للتنفيذ. من وجهة نظر نظام التشغيل ، يعتبر مؤشر ترابط العملية أو العملية أيضًا مؤشر ترابط ذا معنى للتنفيذ.
3.2. تعدد المهام والتزامن
حجر الزاوية في الإنتاجية هو القدرة على القيام بهذه الخدعة: عندما يكون لديّ مؤشر ترابط واحد للتنفيذ يحتوي على فراغات في وقته الفعلي ، ثم ملء هذه الفراغات بشيء مفيد - اتبع خطوات مؤشرات الترابط الأخرى للتنفيذ.

تخدم قواعد البيانات عادةً العديد من العملاء في نفس الوقت. إذا استطعنا الجمع بين العمل على عدة خيوط للتنفيذ في إطار خيط واحد للتنفيذ بمستوى أعلى ، فإن هذا يسمى تعدد المهام. أي أن تعدد المهام هو عندما أقوم بتنفيذ إجراءات في إطار تدفق أكبر واحد من التنفيذ يكون خاضعًا لحل المهام الأصغر.
من المهم عدم الخلط بين مفهوم تعدد المهام والتوازي. التزامن -
هذه هي خصائص بيئة وقت التشغيل ، والتي تجعل من الممكن في خطوة واحدة من الوقت ، في خطوة واحدة ، إحراز تقدم في مؤشرات ترابط التنفيذ المختلفة. إذا كان لدي معالجان فعليان ، فيمكنهما في دورة واحدة على مدار الساعة تنفيذ تعليميين. إذا كان البرنامج يعمل على معالج واحد ، فسيستغرق الأمر دورتين على مدار الساعة لتنفيذ نفس الإرشادات.

من المهم عدم الخلط بين هذه المفاهيم ، لأنها تقع في فئات مختلفة. تعد تعدد المهام إحدى ميزات البرنامج التي يتم تنظيمها داخليًا كعمل متغير في مهام مختلفة. التزامن هو خاصية لبيئة التشغيل التي تمكنك من العمل على العديد من المهام في دورة واحدة على مدار الساعة.
في العديد من الطرق ، تقوم التعليمات البرمجية غير المتزامنة والكتابة غير المتزامنة بكتابة التعليمات البرمجية متعددة المهام. تكمن الصعوبة الرئيسية في كيفية تشفير المهام وكيفية إدارتها. لذلك ، اليوم سوف نتحدث عن هذا - كتابة رمز تعدد المهام.
4. الحجب والانتظار

لنبدأ ببعض الأمثلة البسيطة. العودة إلى كرة الطاولة:
socket s; string x; x = read_from_socket(s, 4); if (x == "ping") { write_to_socket(s, "pong"); } return;
كما ناقشنا بالفعل ، بعد أن يقرأ خيوط التنفيذ والخطوط البيضاء نائمًا ، يتم حظره. عادة ما نقول ، "التدفق محظور."
socket s; string x; x = read_from_socket(s, 4); if (x == "ping") { write_to_socket(s, "pong"); } return;
هذا يعني أن تدفق التنفيذ قد وصل إلى نقطة حيث أي حدث ضروري لمواصلة ذلك. على وجه الخصوص ، في حالة تطبيق شبكتنا ، من الضروري أن تصل البيانات عبر الشبكة ، أو على العكس ، لدينا مخزن مؤقت مجاني لكتابة البيانات إلى الشبكة. الأحداث قد تكون مختلفة. إذا كنا نتحدث عن الجوانب الزمنية ، فيمكننا الانتظار حتى يتم تشغيل المؤقت أو إكمال عملية أخرى. الأحداث هنا هي نوع من الأشياء المجردة ، من المهم أن نفهم أنه يمكن توقعها.

عندما نكتب رمزًا بسيطًا ، فإننا نمنح ضمنيًا التحكم في توقع الحدث إلى مستوى أعلى. في حالتنا ، ونظام التشغيل. هي ، بصفتها كيانًا بمستوى أعلى ، مسؤولة عن اختيار المهمة التي سيتم تنفيذها بعد ذلك ، وهي أيضًا مسؤولة عن تتبع حدوث الأحداث.
كودنا ، الذي نكتبه كمطورين ، منظم في نفس الوقت فيما يتعلق بالعمل في مهمة واحدة. يعالج مقتطف الشفرة من المثال اتصالًا واحدًا: يقرأ ping من اتصال واحد ويكتب pong إلى اتصال واحد.
الكود واضح يمكنك قراءتها وفهم ما الذي تفعله ، وكيف تعمل ، والمشكلة التي تحلها ، وما هي الثغرات الموجودة لديها ، وما إلى ذلك. في الوقت نفسه ، نحن ندير تخطيط المهام بشكل سيء للغاية في مثل هذا النموذج. بشكل عام ، تحتوي أنظمة التشغيل على مفاهيم للأولويات ، ولكن إذا كتبت أنظمة في الوقت الفعلي ، فأنت تعلم أن الأدوات المتوفرة في Linux ليست كافية لإنشاء ما يكفي من أنظمة الوقت الحقيقي المعقولة.
علاوة على ذلك ، يعد نظام التشغيل أمرًا معقدًا ، ويكلف تبديل السياق من تطبيقنا إلى النواة بضعة ميكروثانية ، والتي ، مع بعض العمليات الحسابية البسيطة ، تعطينا تقديرًا لحوالي 20-100 ألف تبديل للسياق في الثانية. هذا يعني أنه إذا قمنا بكتابة خادم ويب ، فيمكننا في غضون ثانية واحدة معالجة حوالي 20 ألف طلب ، على افتراض أن معالجة الطلبات أعلى عشر مرات من النظام.

4.1. عدم حظر الانتظار

إذا وصلت إلى الموقف الذي تحتاجه للعمل مع الشبكة بشكل أكثر كفاءة ، فإنك تبدأ في البحث عن المساعدة على الإنترنت وتنتقي تحديد / epoll. مكتوب على الإنترنت أنه إذا كنت ترغب في خدمة الآلاف من الاتصالات في نفس الوقت ، فأنت بحاجة إلى epoll ، لأنها آلية جيدة وما إلى ذلك. تفتح الوثائق وترى شيئًا كهذا:
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);
الوظائف التي تحتوي فيها الواجهة إما على الكثير من الواصفات التي تعمل معها (في حالة الاختيار) ، أو العديد من الأحداث التي تمر
عبر حدود التطبيق الخاص بك ، نواة نظام التشغيل التي تحتاج إلى معالجة (في حالة epoll).
يجدر أيضًا إضافة أنه لا يمكنك تحديد / epoll ، ولكن إلى مكتبة مثل libuv ، والتي لن تتضمن أي أحداث في واجهة برمجة التطبيقات ، ولكن سيكون لها العديد من عمليات الاسترجاعات. ستقول واجهة المكتبة: "صديقي العزيز ، قم بتوفير رد اتصال لقراءة المقبس ، والذي سأتصل به عندما تظهر البيانات."
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);
ما الذي تغير مقارنةً برمزنا المتزامن في الفصل السابق؟ أصبح الرمز غير متزامن. هذا يعني أننا أخذنا المنطق في التطبيق لتحديد النقطة الزمنية التي يتم فيها مراقبة الأحداث. مكالمات التحديد / epoll الصريحة هي النقاط التي نطلب من نظام التشغيل الحصول على معلومات حول الأحداث التي حدثت فيها. أخذنا أيضًا في رمز التطبيق الخاص بنا اختيار المهمة التي يجب العمل عليها في المرة القادمة.

من أمثلة الواجهات ، يمكنك أن ترى أن هناك آليتين لإدخال المهام المتعددة. نوع واحد من "سحب" عندما نكون
نستخلص العديد من الأحداث التي ننتظرها ، ومن ثم نتفاعل معها بطريقة أو بأخرى. في هذا النهج ، من السهل إطفاء النفقات العامة بواحد
حدث وبالتالي تحقيق إنتاجية عالية في التواصل حول مجموعة الأحداث التي حدثت. عادةً ، تعتمد جميع عناصر الشبكة مثل تفاعل kernel مع بطاقة الشبكة أو تفاعلك ونظام التشغيل على آليات الاستطلاع.
الطريقة الثانية هي آلية "الدفع" ، عندما يأتي كيان خارجي معين بوضوح ، يقاطع تدفق التنفيذ ويقول: "الآن ، يرجى التعامل مع الحدث الذي وصل لتوه". هذا هو النهج مع رد الاتصال ، مع إشارات يونكس ، مع انقطاع على مستوى المعالج ، عندما يغزو كيان خارجي بوضوح موضوع التنفيذ ويقول: "الآن ، من فضلك ، نحن نعمل على هذا الحدث." لقد ظهر هذا النهج من أجل الحد من التأخير بين حدوث حدث ورد الفعل عليه.
لماذا قد يرغب مطورو C ++ الذين يكتبون ويحلون مشاكل معينة في التطبيق في سحب نموذج حدث إلى الكود الخاص بنا؟ إذا قمنا بسحب وإسقاط العمل في العديد من المهام في التعليمات البرمجية الخاصة بنا وإدارتها ، فبسبب عدم الانتقال إلى kernel والعكس صحيح ، يمكننا العمل بشكل أسرع قليلاً وتنفيذ إجراءات أكثر فائدة لكل وحدة زمنية.
ما الذي يؤدي إليه هذا من حيث الرمز الذي نكتبه؟ خذ nginx ، على سبيل المثال ، خادم HTTP عالي الأداء ، شائع جدًا. إذا قمت بقراءة الكود الخاص به ، فهو مبني على نموذج غير متزامن. الكود من الصعب جدا قراءته. عندما تسأل نفسك عما يحدث بالضبط عند معالجة طلب HTTP واحد ، يتبين أن هناك الكثير من الأجزاء في الشفرة ، متباعدة في ملفات مختلفة ، في زوايا مختلفة من قاعدة الشفرة. تقوم كل جزء بجزء صغير من العمل كجزء من خدمة طلب HTTP بالكامل. على سبيل المثال:
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;
هناك بنية الطلب ، والتي يتم توجيهها إلى معالج الأحداث عند قراءة إشارات المقبس أو الوصول للكتابة. علاوة على ذلك ، يقوم هذا المعالج بالتبديل باستمرار في البرنامج وفقًا لحالة معالجة الطلب. إما أن نقرأ الرؤوس ، أو نقرأ نص الطلب ، أو نطلب البيانات الأولية - بشكل عام ، هناك العديد من الحالات المختلفة.
يصعب قراءة هذا الرمز لأنه يوصف في جوهره من حيث رد الفعل على الأحداث. نحن في مثل هذه الحالة ونرد عليها بطريقة معينة للأحداث التي وقعت. هناك نقص في الصورة الكلية للعملية بأكملها لمعالجة طلب HTTP.
هناك خيار آخر ، يتم استخدامه غالبًا في JavaScript ، وهو إنشاء تعليمة برمجية تستند إلى رد الاتصال عندما نعيد توجيه رد الاتصال إلى استدعاء الواجهة ، حيث يوجد عادةً رد اتصال متداخل آخر لحدث ما ، وما إلى ذلك.
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:
الرمز مجزأ مرة أخرى ، لا يوجد فهم للحالة الحالية لكيفية عملنا على الطلب. يتم نقل الكثير من المعلومات من خلال عمليات الإغلاق ، وتحتاج إلى بذل جهود ذهنية لإعادة بناء منطق معالجة طلب واحد.
وبالتالي ، من خلال إدخال تعدد المهام في الكود الخاص بنا (منطق اختيار مهام العمل وتعددها) ، نحصل على كود فعال والتحكم في ترتيب أولويات المهمة ، لكننا نفقد الكثير منها في سهولة القراءة. يصعب قراءة هذا الرمز ويصعب صيانته.

لماذا؟ افترض أن لدي حالة بسيطة ، على سبيل المثال ، لقد قرأت ملفًا ونقلته عبر الشبكة. في إصدار غير محظور ، تتوافق هذه الحالة مع آلة الحالة الخطية هذه:
- الحالة الأولية
- ابدأ في قراءة ملف ،
- في انتظار الرد من نظام الملفات ،
- كتابة ملف إلى مقبس ،
- الحالة النهائية.
الآن ، دعنا نقول أنني أريد إضافة معلومات من قاعدة البيانات إلى هذا الملف. خيار بسيط:
- الحالة الأولية
- قراءة ملف
- اقرأ الملف
- قراءة من قاعدة البيانات
- قراءة من قاعدة البيانات ،
- أنا أعمل مع المقبس
- كتب إلى المقبس.
يبدو مثل رمز خطي ، لكن عدد الحالات زاد.
ثم تبدأ في التفكير في أنه سيكون من الجيد موازاة الخطوتين - القراءة من ملف ومن قاعدة بيانات. تبدأ معجزات التوافقية: أنت في الحالة الأولية ، وتطلب قراءة الملف والبيانات من قاعدة البيانات. ثم يمكنك الوصول إلى حالة توجد فيها بيانات من قاعدة البيانات ، ولكن لا يوجد ملف ، أو العكس - هناك بيانات من الملف ، ولكن ليس من قاعدة البيانات. بعد ذلك ، تحتاج إلى الدخول في حالة يكون لديك فيها أحد أمرين. مرة أخرى ، هذه دولتان. ثم تحتاج إلى الذهاب إلى حالة حيث لديك كل المكونات. ثم اكتبها إلى المقبس وهلم جرا.
كلما كان التطبيق أكثر تعقيدًا ، زاد عدد الحالات ، زاد عدد أجزاء التعليمات البرمجية التي يجب دمجها في رأسك. غير مريحة. أو كنت تكتب الشعرية رد الاتصال ، وهو غير مريح للقراءة. إذا كان نظام المتفرعة مكتوبًا ، فحينئذٍ يأتي يوم ما لم يعد بإمكانك تحمله.
5. العقود الآجلة / الوعود

لحل المشكلة ، تحتاج إلى النظر إلى الموقف بشكل أسهل.

يوجد برنامج به دوائر سوداء وحمراء. تدفق التنفيذ لدينا هو دوائر سوداء. في بعض الأحيان يتناوبون مع اللون الأحمر عندما يتعذر على الدفق مواصلة عمله. المشكلة هي أنه بالنسبة لخيط التنفيذ الأسود الخاص بنا ، يجب عليك الدخول إلى الدائرة السوداء التالية ، والتي لن تكون معروفة متى.
المشكلة هي أنه عندما نكتب الشفرة بلغة برمجة ، فإننا نوضح للكمبيوتر ما يجب القيام به الآن. الكمبيوتر هو شيء بسيط نسبياً ويتوقع الإرشادات التي نكتبها بلغة البرمجة. إنها تنتظر التعليمات للدائرة التالية ، وفي لغتنا البرمجية لا يوجد ما يكفي من المال ليقول: "في المستقبل ، من فضلك ، عندما يحدث شيء ما ، افعل شيئًا ما."

في لغة البرمجة ، نعمل بإجراءات لحظية مفهومة: استدعاء دالة ، العمليات الحسابية ، إلخ. يصفون الخطوة التالية التالية المحددة. في الوقت نفسه ، لمعالجة منطق التطبيق ، من الضروري أن لا تصف الخطوة الفعلية التالية ، ولكن الخطوة المنطقية التالية: ماذا يجب أن نفعل عندما تظهر البيانات من قاعدة البيانات ، على سبيل المثال.

لذلك ، نحن بحاجة إلى بعض الآلية كيفية الجمع بين هذه الشظايا. في الحالة التي كتبنا فيها رمزًا متزامنًا ، قمنا بإخفاء السؤال بالكامل أسفل الغطاء وقلنا إن نظام التشغيل سيتعامل معه ، ويسمح له بمقاطعة وإعادة جدولة مؤشرات الترابط الخاصة بنا.
في المستوى 1 ، افتتحنا مربع Pandora ، وجلب الكثير من التبديل والحالة والظروف والفروع إلى الكود. أريد بعض التنازلات بحيث تكون الشفرة قابلة للقراءة نسبيًا ، ولكنها تحتفظ بجميع مزايا المستوى 1.
لحسن الحظ بالنسبة لنا ، في عام 1988 ، أدرك الأشخاص المشاركون في النظم الموزعة ، باربرا ليسكوف ولوبا شيرير ، المشكلة ، وتوصلوا إلى الحاجة إلى تغييرات لغوية. من الضروري إضافة تصميمات إلى لغة البرمجة التي تسمح بالتعبير عن العلاقات الزمنية بين الأحداث - في الوقت الحالي من الوقت وفي لحظة غير مؤكدة في المستقبل.
وتسمى هذه الوعود. المفهوم رائع ، لكنه يجمع الغبار على الرف لمدة عشرين عامًا. — , 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. أمثلة
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.
, . كيف نفعل ذلك؟
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. . , .
: , , , . , , , , . ? , .
دقيقة من الإعلانات. 19-20 C++ Russia 2019. , , Grimm Rainer «Concurrency and parallelism in C++17 and C++20/23» , C++ . , . , , - .