6 min. read

הקדמה

ל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(500ms);
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(2s);
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(5s);
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(500ms);
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> // Windows Threadpool API

using namespace std;
using namespace std::literals;

// operator overload.
// co_await can't use primitive type parameter.
auto operator co_await(chrono::system_clock::duration duration)
{
using namespace std::experimental;

// Awaitable must implements 3 function.
// - bool await_ready();
// - auto await_suspend();
// - T await_resume();
class awaiter
{
static
void CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE,
void* Context,
PTP_TIMER)
{
// Callback Thread will resume the function
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);
}

// If not ready (`false`), invoke `await_suspend`
// If ready (`true`), go to `await_resume` directly.
bool await_ready() const
{
return duration.count() <= 0;
}

// Return might be ignored.
bool await_suspend(coroutine_handle<> resume_cb)
{
int64_t relative_count = -duration.count();
timer = CreateThreadpoolTimer(TimerCallback,
resume_cb.address(),
nullptr);
// Set the timer and then suspend...
SetThreadpoolTimer(timer, (PFILETIME)&relative_count, 0, 0);
return timer != 0;
}

// Return T type's value after resumed.
// T can be `void`.
void await_resume() {}

};
return awaiter{ duration };
}

// Resumable Function
future<void> test()
{
cout << this_thread::get_id() << ": sleeping…\n";

// await for 5 seconds ...
co_await 5000ms;

cout << this_thread::get_id() << ": woke up\n";
}

// This is normal subroutine
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

תודה על הקריאה!


אהבתם? מוזמנים להביע תמיכה כאן: כוס קפה