הקדמה לC++
לא היה סטנדרט לתהליכונים או אסינכרוניות עד לגרסה 11. מתכנתים היו צריכים להסתמך על ספריות חיצוניות כדי לעשות את העבודה הזו.
בC++ 11
יצאו מגוון רחב של האדרים שאפשרו לבצע קוד מקבילי ואסינכרוני. בפוסט הזה נתמקד בהאדר future
וב-std::async(...)
.
נראה איך בקלות נוכל להריץ קוד אסינכורני ולחכות לתוצאה שלו!
ובסוף נקשר את זה לתצורה חדשה של co routine
שנוספו ב-C++ 20
. ד”א הקונספטים האלו קיימים הרבה זמן, קו-רוטינה זה משהו שחשבו עליו עוד בשנות ה60 וה70.
include Future future
מוסיף יכולות אסינכרוניות ומאפשר להריץ קוד, לחכות לתשובה או לשגיאה.
הדרך הכי פשוטה להריץ קוד אסינכרוני הוא בעזרת std::async
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <future> #include <iostream> #include <chrono> int main () { using namespace std::chrono_literals; auto printHi = []() { std::this_thread::sleep_for (500 ms); auto tId = std::this_thread::get_id (); std::cout << "Hello from " << tId << "\n" ; }; printHi (); auto fut = std::async (std::launch::async, printHi); return 0 ; }
להרצה הזו יש 2 אופציות:
async - מריץ את הקוד אסינכרוני.
deferred - מריץ את הקוד בצורה עצלנית - בפעם הראשונה שמישהו ייבקש את התוצאה.
1 auto fut = std::async (std::launch::async, printHi);
async
מחזיר אובייקט future
שעליו ניתן לחכות. ניתן לחכות לאובייקט הזה בקריאה ל- future.wait()
, או לחכות שהוא שייעבור destructor והתוצאה תבוקש.
אם נרצה להריץ את הקוד מאוחר יותר בצורה עצלנית נוכל לקרוא ישירות ל-get
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <future> #include <iostream> #include <chrono> int main () { using namespace std::chrono_literals; auto printHi = []() { auto tId = std::this_thread::get_id (); std::cout << "Hello from " << tId << "\n" ; }; printHi (); auto fut = std::async (std::launch::deferred, printHi); std::cout << "Waiting 2 seconds...\n" ; std::this_thread::sleep_for (2 s); std::cout << "Calling for deferred\n" ; fut.get (); return 0 ; }
מה שהודפס:
1 2 3 4 Hello from 3600 Waiting 2 seconds... Calling for deferred Hello from 3600
שימו לב שזה רץ על אותו תהליכון הפעם :)
Promise אובייקט ה-promise
מאחסן ערך עבור פעולה אסינכרונית.
בדוגמא פה אנחנו מבצעים חישוב אסינכרוני ושומרים את הערך ב-promise
. בכך שאנחנו מוציאים החוצה future
ולא - promise
אנחנו יכולים לשלוט על התוצאה האסינכרונית. בעוד לתת אינדיקציה למשתמש שהפעולה שלנו הסתיימה!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <future> #include <iostream> #include <chrono> using namespace std::chrono_literals;using MyHash = unsigned long long ;struct HasherWaiter { std::future<MyHash> operator () (std::string name) { mHashingFuture = std::async (std::launch::async, [&]() { std::this_thread::sleep_for (5 s); std::hash<std::string> h; auto hash = (MyHash)h (name); mPromise.set_value (hash); }); return mPromise.get_future (); } private : std::future<void > mHashingFuture; std::promise<MyHash> mPromise; }; int main () { HasherWaiter hahser; auto future = hahser ("Hello World" ); while (!future._Is_ready()) { std::this_thread::sleep_for (500 ms); std::cout << "Waiting for hash...\n" ; } std::cout << "Hash is " << future.get () << "\n" ; return 0 ; }
packaged_task האובייקט packaged_task
אורז פונקציה או פעולה אחרת מכל סוג שהוא לכדי פעולה אסינכרונית. וחושף בפנינו future
שנוכל להשתמש בו כדי לחזור לתוצאה האסינכרונית.
בדוגמא הזו מתבצע חישוב מפונקציה אחרת, בעזרת bind
הפונקציה מוחדרת לכל ה-packaged_task
. באותו אופן היינו יכולים לעטוף פונקציות API
של לינוקס או ווינדוס. או אם מדובר ב-API
חיצוני שאין לנו דרך לשנות אותו, אנחנו יכולים ככה לעטוף אותו ולהוסיף אסינכרוניות לקוד שלנו.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <future> #include <iostream> #include <chrono> int RectangleArea (int width, int height) { return width * height; }struct Calculator { Calculator () : mTask (std::bind (RectangleArea, 55 , 22 )) { } std::future<int > CalculateArea () { mTask (); return mTask.get_future (); } private : std::packaged_task<int ()> mTask; }; int main () { Calculator calc; auto result = calc.CalculateArea (); std::cout << "Area: " << result.get () << "\n" ; return 0 ; }
std::async
כל כך קל ונוח לשימוש. בעזרת האובייקטים הנוספים אנחנו יכולים לעטוף קוד לא אסינכרוני וליצור מקביליות בקלות יותר.
C++ 20 מוסיפה יכולות חדשות שנקראות - co routine
.
Co routine הדגמה אני לא אסביר יותר מדי, אך נעבור על קוד לראות איך זה נראה.
MSVC תבנו על C++ הגרסא הכי חדשה. כדי לבנות צריך להוסיף /await
לקומפיילר.
GCC g++ -fcoroutines -std=c++20
CLANG clang++ -std=c++20 -stdlib=libc++ -fcoroutines-ts
Genertor ג’נרטור כמו בפייתון הוא אובייקט המאפשר בצורה עצלנית ליצור ערכים:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <iostream> #include <experimental/coroutine> #include <experimental/generator> #include <coroutine> std::experimental::generator<int > WalkTo (int n) noexcept { auto i = 0 ; while (i < n) { co_yield i; i++; } } int main () { auto g = WalkTo (5 ); auto itr = g.begin (); std::cout << *itr << "\n" ; itr++; std::cout << *itr << "\n" ; itr++; std::cout << *itr << "\n" ; itr++; return 0 ; }
co_await זה הולך להיות קוד מעניין. הקוד מהבלוג של MSVC
.
ממליץ לקרוא פה:https://devblogs.microsoft.com/cppblog/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 #include <iostream> #include <chrono> #include <future> #include <windows.h> using namespace std;using namespace std::literals;auto operator co_await (chrono::system_clock::duration duration) { using namespace std::experimental; class awaiter { static void CALLBACK TimerCallback (PTP_CALLBACK_INSTANCE, void * Context, PTP_TIMER) { coroutine_handle<>::from_address (Context).resume (); } PTP_TIMER timer = nullptr ; chrono::system_clock::duration duration; public : explicit awaiter (chrono::system_clock::duration d) : duration(d) { } ~awaiter () { if (timer) CloseThreadpoolTimer (timer); } bool await_ready () const { return duration.count () <= 0 ; } bool await_suspend (coroutine_handle<> resume_cb) { int64_t relative_count = -duration.count (); timer = CreateThreadpoolTimer (TimerCallback, resume_cb.address (), nullptr ); SetThreadpoolTimer (timer, (PFILETIME)&relative_count, 0 , 0 ); return timer != 0 ; } void await_resume () {} }; return awaiter{ duration }; } future<void > test () { cout << this_thread::get_id () << ": sleeping…\n" ; co_await 5000 ms; cout << this_thread::get_id () << ": woke up\n" ; } void usecase () { test ().get (); cout << this_thread::get_id () << ": back in main\n" ; } int main () { usecase (); return 0 ; }
לפונקציית קו-רוטינה צריכה להיות 4 פעולות:
מה ששונה בין קו-רוטינה לסאב-רוטינה זה שלסאב-רוטינה אין יכולה להפעיל ולהשעות את עצמה.
co_await
דורשת 3 פונקציות בתור הממשק שלה:
await_ready
await_suspend
await_resume
future
מקיים את זה בעזרת _Future_awaiter
, ולכן נוכל לעשות אדפטציה כזו בין future
ל-awaiter
.
ב-C++11
:std::async
ו -future
נותנים לנו אדפטציה קלה לפונקציות אסינכרוניות.
עכשיו ב-C++ 20
העלנו שלב ומשלבים את כל הקונספטים לכדי הרצה פשוטה יותר וגנרית יותר של קוד אסינכרוני ומקבילי.
במה זה ייעזור לנו?
לבנות שרתי רשת יותר יעילים.
לבנות משחקים יותר טובים.
אלגוריתמיקה יותר מהירה
AI פשוט יותר
לקריאה מעמיקה יותר בנושא הקו-רוטינות:
https://luncliff.github.io/coroutine/articles/exploring-msvc-coroutine/#awaitable-interface
https://en.cppreference.com/w/cpp/language/coroutines
https://www.scs.stanford.edu/~dm/blog/c++-coroutines.html
תודה על הקריאה!