במדריך הזה מתואר מודל השרשור שבו נעשה שימוש בספריות הלקוח C++, ומוסבר איך לשנות את ברירת המחדל של מאגרי השרשורים באפליקציה.
מטרות
- תאר את מודל השרשור שמוגדר כברירת מחדל בספריות הלקוח של C++.
- לתאר איך אפשר לבטל את הגדרות ברירת המחדל האלה באפליקציות שצריך.
למה ספריות הלקוח משתמשות בשרשורים ברקע?
רוב הפונקציות בספריות הלקוח משתמשות בשרשור שקורא לפונקציה כדי להשלים את כל העבודה, כולל קריאות RPC לשירות ו/או רענון של אסימוני גישה לאימות.
פונקציות אסינכרוניות, מעצם טבען, לא יכולות להשתמש בשרשור הנוכחי כדי להשלים את העבודה שלהן. חלק מהשרשורים הנפרדים צריכים לחכות לסיום העבודה ולטפל בתגובה.
בנוסף, חסימת השרשור של השיחה בפעולות שפועלות לאורך זמן היא בזבוז, כי יכול להיות שיחלפו דקות או יותר עד שהשירות יסיים את העבודה. בפעולות כאלה, ספריית הלקוח משתמשת בשרשורים ברקע כדי לבדוק מעת לעת את מצב הפעולה הממושכת.
באילו פונקציות וספריות נדרשים שרשורים ברקע?
פונקציות שמחזירות future<T> עבור סוג מסוים T משתמשות בשרשורים ברקע כדי להמתין עד שהעבודה תושלם.
לא בכל ספריות הלקוח יש פונקציות אסינכרוניות או פעולות ממושכות. ספריות שלא צריכות אותם לא יוצרות שרשורים ברקע.
יכול להיות שתבחינו בשרשורים נוספים באפליקציה, אבל יכול להיות שהם נוצרו על ידי יחסי תלות של ספריית הלקוח של C++, כמו gRPC. בדרך כלל אין עניין בשרשורים האלה, כי אף פעם לא מופעל בהם קוד של אפליקציה והם משמשים רק לפונקציות משניות.
איך השרשורים האלה ברקע משפיעים על האפליקציה שלי?
כרגיל, השרשורים האלה מתחרים על משאבי המעבד והזיכרון עם שאר האפליקציה. אם צריך, אפשר ליצור מאגר משלכם של שרשורים כדי לקבל שליטה מדויקת במשאבים שבהם השרשורים האלה משתמשים. פרטים נוספים מופיעים בהמשך.
האם חלק מהקוד שלי פועל באחד מהשרשורים האלה?
כן. כשמצרפים קריאה חוזרת ל-future<T>, הקריאה החוזרת כמעט תמיד מופעלת על ידי אחד משרשורי הרקע. המקרה היחיד שבו זה לא יקרה הוא אם התנאי של future<T> כבר מתקיים בזמן שמצרפים את פונקציית הקריאה החוזרת. במקרה כזה, הקריאה החוזרת מופעלת באופן מיידי, בהקשר של השרשור שמצורפת אליו הקריאה החוזרת.
לדוגמה, נניח שיש אפליקציה שמשתמשת בספריית הלקוח של Pub/Sub.
השיחה Publish() מחזירה ערך עתידי והאפליקציה יכולה לצרף קריאה חוזרת (callback) אחרי ביצוע פעולה מסוימת:
namespace pubsub = ::google::cloud::pubsub;
namespace g = google::cloud;
void Callback(g::future<g::StatusOr<std::string>>);
void F(pubsub::Publisher publisher) {
auto my_future = publisher.Publish(
pubsub::MessageBuilder("Hello World!").Build());
// do some work.
my_future.then(Callback);
}
אם התנאי my_future מתקיים לפני הקריאה לפונקציה .then(), הקריאה החוזרת מופעלת באופן מיידי. אם רוצים להבטיח שהקוד יפעל בשרשור נפרד, צריך להשתמש במאגר שרשורים משלכם ולספק פונקציה שניתן להפעיל ב-.then(), שתעביר את ההפעלה למאגר השרשורים שלכם.
מאגרי ברירת מחדל של פרוטוקולי Thread
בספריות שדורשות שרשורים ברקע, הפונקציה Make*Connection()יוצרת מאגר שרשורים שמוגדר כברירת מחדל. אלא אם מבטלים את ברירת המחדל של מאגר השרשורים, לכל אובייקט *Connection יש מאגר שרשורים נפרד.
מאגר השרשורים שמוגדר כברירת מחדל ברוב הספריות מכיל שרשור יחיד. בדרך כלל לא צריך יותר שרשורים, כי השרשור ברקע משמש לדגימת הסטטוס של פעולות ממושכות. השיחות האלה קצרות יחסית וצורכות מעט מאוד CPU, כך ששרשור רקע יחיד יכול לטפל במאות פעולות ארוכות טווח בהמתנה, ומעט מאוד אפליקציות כוללות אפילו כל כך הרבה פעולות.
יכול להיות שלפעולות אסינכרוניות אחרות יידרשו יותר משאבים.
אם צריך, משתמשים ב-GrpcBackgroundThreadPoolSizeOption כדי לשנות את גודל ברירת המחדל של מאגר השרשורים ברקע.
ספריית Pub/Sub צפויה לבצע הרבה יותר עבודה, כי בדרך כלל אפליקציות Pub/Sub מקבלות או שולחות אלפי הודעות בשנייה. לכן, בספרייה הזו מוגדר כברירת מחדל שרשור אחד לכל ליבה בארכיטקטורות של 64 ביט. בארכיטקטורות של 32 ביט (או כשמבצעים קומפילציה במצב 32 ביט, גם אם מריצים בארכיטקטורה של 64 ביט), ברירת המחדל הזו משתנה ל-4 תהליכים בלבד.
הוספת מאגר של פרוטוקולי Thread משלכם
אתם יכולים לספק מאגר שרשורים משלכם לשרשורים ברקע. יוצרים אובייקט CompletionQueue, מצרפים אליו שרשורים ומגדירים את GrpcCompletionQueueOption בזמן האתחול של הלקוח. לדוגמה:
namespace admin = ::google::cloud::spanner_admin;
namespace g = ::google::cloud;
void F() {
// You will need to create threads
auto cq = g::CompletionQueue();
std::vector<std::jthread> threads;
for (int i = 0; i != 10; ++i) {
threads.emplace_back([](auto cq) { cq.Run(); }, cq);
}
auto client = admin::InstanceAdminClient(admin::MakeInstanceAdminConnection(
g::Options{}.set<g::GrpcCompletionQueueOption>(cq)));
// Use `client` as usual
}
אפשר לשתף את אותו אובייקט CompletionQueue בין כמה לקוחות, גם בשירותים שונים:
namespace admin = ::google::cloud::spanner_admin;
namespace pubsub = ::google::cloud::pubsub;
namespace g = ::google::cloud;
void F(pubsub::Topic const& topic1, pubsub::Topic const& topic2) {
// You will need to create threads
auto cq = g::CompletionQueue();
std::vector<std::jthread> threads;
for (int i = 0; i != 10; ++i) {
threads.emplace_back([](auto cq) { cq.Run(); }, cq);
}
auto client = admin::InstanceAdminClient(admin::MakeInstanceAdminConnection(
g::Options{}.set<g::GrpcCompletionQueue>(cq)));
auto p1 = pubsub::Publisher(pubsub::MakePublisherConnection(
topic1, g::Options{}.set<g::GrpcCompletionQueueOption>(cq)));
auto p2 = pubsub::Publisher(pubsub::MakePublisherConnection(
topic2, g::Options{}.set<g::GrpcCompletionQueueOption>(cq)));
// Use `client`, `p1`, and `p2` as usual
}
השלבים הבאים
- במאמר הגדרת ספריות לקוח אפשר לקרוא מידע נוסף על אפשרויות נפוצות להגדרת ספריות.
- מאמרי העזרה המלאים של ספריית הלקוח של Pub/Sub זמינים במאמר ספריית הלקוח של Cloud Pub/Sub עבור C++.