חוזה פעולה חופפת¶
נניח שאנחנו קוראים קובץ באמצעות ReadFile והקריאה חוזרת מיד. האם מותר לשחרר את ה־buffer? התשובה תלויה במה ערך ההחזרה אומר. בפתיחה עם FILE_FLAG_OVERLAPPED, החזרה יכולה לציין השלמה מיידית, פעולה שעדיין מתבצעת או כשל שלא התחיל פעולה ממתינה.
נבנה בקשה אחת עם אירוע השלמה משלה. ה־buffer, מבנה OVERLAPPED וה־handle נשארים בחיים עד שהשלמת הבקשה נקבעה. הדוגמה אינה מבטיחה שהקריאה תהיה איטית, ולכן תוצאת ניסוי יכולה להיות השלמה מיידית גם אם ביקשנו לבדוק ביטול.
בקשה עם בעלות ברורה¶
התוכנית הבאה קוראת עד ארבעת אלפים תשעים ושישה בתים מתחילת קובץ מקומי. היא דורשת נתיב אחד, ופותחת את הקובץ לקריאה בלבד:
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <array>
#include <cstddef>
#include <iostream>
struct Handle {
HANDLE value;
~Handle() {
if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value);
}
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
explicit Handle(HANDLE v) : value(v) {}
};
int wmain(int argc, wchar_t** argv) {
if (argc != 2) return 2;
Handle file(CreateFileW(argv[1], GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING,
FILE_FLAG_OVERLAPPED, nullptr));
if (file.value == INVALID_HANDLE_VALUE) {
std::cerr << "open: " << GetLastError() << '\n';
return 1;
}
Handle event(CreateEventW(nullptr, TRUE, FALSE, nullptr));
if (!event.value) {
std::cerr << "event: " << GetLastError() << '\n';
return 1;
}
std::array<std::byte, 4096> buffer{};
OVERLAPPED operation{};
operation.hEvent = event.value;
DWORD ignored = 0;
const BOOL immediate = ReadFile(file.value, buffer.data(),
static_cast<DWORD>(buffer.size()), &ignored, &operation);
if (immediate) {
std::cout << "bytes=" << ignored << '\n';
return 0;
}
if (!immediate) {
const DWORD error = GetLastError();
if (error != ERROR_IO_PENDING) {
if (error == ERROR_HANDLE_EOF) return 0;
std::cerr << "submit: " << error << '\n';
return 1;
}
const DWORD wait = WaitForSingleObject(event.value, 10);
if (wait != WAIT_OBJECT_0) {
if (wait == WAIT_FAILED)
std::cerr << "wait: " << GetLastError() << '\n';
if (!CancelIoEx(file.value, &operation)) {
const DWORD error = GetLastError();
if (error != ERROR_NOT_FOUND)
std::cerr << "cancel request: " << error << '\n';
}
}
}
DWORD transferred = 0;
const BOOL completed = GetOverlappedResult(
file.value, &operation, &transferred, TRUE);
if (!completed) {
const DWORD error = GetLastError();
std::cerr << "completion: " << error << '\n';
return error == ERROR_OPERATION_ABORTED ? 3 : 1;
}
std::cout << "bytes=" << transferred << '\n';
}
ה־OVERLAPPED מאותחל לאפס, כולל offset אפס. האירוע manual-reset שייך לבקשה הזאת בלבד. אין קוד אחר שמסמן אותו, ולכן GetOverlappedResult עם המתנה יכול לשמש כדי לאסוף את תוצאת הבקשה. בקשה חדשה תקבל מבנה נפרד, או תשמש מחדש את המבנה רק אחרי סיום מלא והכנה מחדש.
שלוש תוצאות של הגשה¶
החזרה של true מציינת שהפעולה הושלמה מיידית לפי חוזה ReadFile. החזרה של false עם ERROR_IO_PENDING מציינת שהפעולה התקבלה ועדיין יש לאסוף את השלמתה. שגיאת הגשה אחרת אינה אותו מצב, ואין להמתין לנצח לאירוע של בקשה שלא הוגשה.
גם הצלחה אינה מבטיחה שכל הגודל שהתבקש הועבר. הספירה שהתקבלה היא חלק מהתוצאה. בקריאה מקובץ קצר אפשר לקבל פחות מן הגודל המבוקש, והטיפול בסוף קובץ תלוי במסלול וב־API. אסור לעבד את כל ה־buffer רק משום שהוא הוקצה בגודל גדול יותר.
ה־OVERLAPPED אינו משתנה מקומי שאפשר למסור ולשכוח. המערכת יכולה להמשיך לגשת אליו לאחר החזרה, וכך גם אל ה־buffer. העברתו בתוך vector שגדל ומשנה כתובות בזמן פעולה ממתינה יכולה לשבור את החוזה אפילו בלי delete מפורש.
ביטול הוא בקשה שמתחרה עם השלמה¶
אחרי עשרה מילישניות נבקש ביטול אם האירוע עדיין לא הושלם. ייתכן שבדיוק באותו רגע הקריאה הסתיימה. הפונקציה CancelIoEx יכולה להחזיר ERROR_NOT_FOUND כאשר לא נמצאה בקשה מתאימה לביטול. הממצא הזה אינו תחליף לאיסוף ההשלמה, משום שהבקשה אולי הסתיימה במקביל.
גם הצלחה של CancelIoEx אינה אומרת שה־buffer כבר פנוי. הביטול צריך להגיע למסלול המבצע, והשלמה יכולה להגיע כהצלחה רגילה, כשגיאת ביטול או כשגיאה אחרת. הקוד ממתין לתוצאה הסופית לפני יציאה מן התחום שמחזיק את הזיכרון.
ההמתנה הסופית אינה מוגבלת לעשר מילישניות. זהו מחיר מפורש של חוזה החיים בדוגמה: לא נשחרר זיכרון שהמערכת אולי משתמשת בו רק כדי לעמוד ב־timeout. מערכת שצריכה גבול זמן קשיח יכולה לבודד את העבודה בתהליך מתאים או להשתמש בחוזה התקן שמבטיח התקדמות. אין הבטחה כללית שכל driver יבטל כל פעולה בזמן מוגבל.
ביטול ופרסום תוצאה¶
נניח שהמשתמש כבר קיבל הודעת ביטול, אבל הקריאה הושלמה בהצלחה רגע אחר כך. יש להפריד בין תוצאת מערכת ההפעלה לבין החלטת הממשק. אפשר להחליט שתוצאה שהגיעה אחרי ביטול אינה מתפרסמת, אך עדיין צריך לאסוף אותה ולנקות את המשאבים.
במנוע עם בקשות רבות נוסיף מזהה בקשה, מצב סיום יחיד ובעלות שמתמשכת עד ההשלמה. ניסיון לשחרר את ה־context גם במסלול timeout וגם במסלול completion ייצור double free. השיעור הבא יחליף את האירוע הפרטי בתור השלמות משותף, בלי לשנות את חובת החיים הזאת.
קריאה להעמקה¶
החוזים של פעולות חופפות וסנכרון ושל ביטול בקשות ממתינות מגדירים מתי מותר להשתמש שוב במבנה וב־buffer.