תהליכון רקע היה מייצא דוח של 40,000 שורות כשתהליכון ה־UI קבע תא אחד, והקובץ שנחת על הדיסק לא תאם אף חוברת שהתקיימה אי פעם. HotXLS מטפל במחלקת הבאגים הזאת ב־lxWorkbookView.pas, שם IXLSWorkbookViewCore מנפיק חוזי קריאה של O(1) ושומרי כתיבה שנכשלים מהר: בזמן שחוזה פתוח, כל נקודת כניסת מוטציה זורקת במקום לכתוב
הכשל שמגיע בלי מחסנית קריאה
קריאת חוברת אינה פעולה אטומית אחת לעולם. מהלך דוח הוא עשרות אלפי קריאות תא בודדות שמתפרשות על שניות, ו־SetValue אחד שנוחת בין שתיים מהן מספיק לשנות את מה ששאר המהלך רואה. המנוע הקלאסי ממחיש זאת: TXLSCellRef.SetValue יכול לקרוא ל־FSST.Remove כדי להסיר רשומת מחרוזת משותפת, לאפס את FValueType ולפסול מצב מטמון נוסחה — והכול בזמן שתהליכון אחר באמצע דרפרנס בדיוק לאותן מבנים. שום דבר לא קורס במקום. אתם מקבלים דוח שסיכומי המשנה שלו אינם מסתדרים, או ייצוא שקורא בשקט מדד מחרוזת שמצביע עכשיו למקום אחר
HotXLS במכוון אינו פותר זאת בכך שהוא מגרם לכותבים לחכות. קורא יכול להחזיק חוברת משך שניות אחדות, ובאפליקציית VCL הכותב הוא לעיתים קרובות קולבק UI או מטפל אירוע בתהליכון הראשי — חסימת התהליכון הזה עד לסיום ייצוא רקע היא תוצאה גרועה מכשל של העריכה. לכן ליבת התיאום זורקת EXLSWorkbookWriteGuardUnavailable ברגע שניסיון כתיבה נעשה כנגד חוזה פתוח, לפני ששדה אחד נגע, והקורא מחליט אם לתור את העריכה, לנסות שוב, או לספר למשתמש. התנגשויות נכשלות מהר, לא מתורסמות
האם חוברת בטוחה לקריאה משני תהליכונים?
כן, בתנאי ששני הקוראים מחזיקים חוזה ואף אחד לא כותב. IXLSWorkbookViewCore.AcquireReadLease נוטל TCriticalSection, מגדיל מניין, מצלם את הדור הנוכחי ומחזיר IXLSWorkbookReadLease — בזמן קבוע ללא תלות בשאלה אם החוברת מחזיקה אלף תאים או מיליון. כל מספר של חוזים מתקיים יחד, אפשר לשחרר אותם בכל סדר, וכל אחד מצמיד את הליבה לחיים באמצעות הפניית הממשק שלו, כך שחוזה שמאריך ימים מעבר לאובייקט שיצר אותו הוא בטוח ולא מצביע תלוש. שני המנועים משתתפים: TXLSWorkbook ב־lxHandle.pas ו־TXLSXWorkbook ב־lxHandleX.pas כל אחד בונה ליבה בבנאי שלו וחושף _AcquireReadLease ו־_AcquireWriteGuard
מה שחשוב לא פחות הוא מה שהחוזה אינו מוסיף לנתיב הקריאה. הקטע הקריטי מכסה נטילת חוזה, שחרור חוזה וגבולות עסקת כתיבה — שום דבר אחר. קריאת התא הרגילה לעולם אינה נכנסת למנעול, למוניטור או למניין אטומי, כך שהחזקת חוזה עולה נטילה אחת ושחרור אחד לכל הסריקה, לא אחד לכל תא. זה אותו אינסטינקט עיצוב שמאחורי עבודת ניתוח XLSX מקבילי ומקצה הזיכרון: לשלם עבור תיאום בגבול, לעולם לא בלולאה הפנימית. הכלל הסימטרי אף הוא מתקיים — AcquireReadLease זורק EXLSWorkbookReadLeaseUnavailable בכל פעם שה־WriteDepth אינו אפס, כך שאי אפשר לפתוח חוזה מתוך עסקת כתיבה, לא אפילו בתהליכון הכותב
uses
lxHandle, lxWorkbookView;
procedure TReportThread.Execute;
var
Lease: IXLSWorkbookReadLease;
Sheet: TXLSWorksheet;
Row: Integer;
Total: Double;
begin
// זורק EXLSWorkbookReadLeaseUnavailable אם כתיבה בעיצומה
Lease := FWorkbook._AcquireReadLease;
Sheet := FWorkbook.Sheets[1];
Total := 0;
for Row := 1 to 50000 do
Total := Total + Sheet.Cells[Row, 3].Value;
FTotal := Total;
// החוזה יוצא מהיקף כאן: מניין ההפניות שלו צונח לאפס,
// ReleaseReadLease רץ, והכותבים הופכים אפשריים שוב
end;היכן שומר הכתיבה יושב בפועל?
בשכבה בת השינוי הנמוכה ביותר, לעולם לא ב־API הנוחות מעליה. _AcquireWriteGuard נקרא מתוך TXLSCellRef.SetValue עצמו, כלומר כל נתיב ציבורי שנשפך אליו — Range.Value, השמת טקסט גיליון, העתקה תא־תא, הדבקה — נשען פעם אחת במקום שכל מעטפת תחזור על בדיקה שמעטפת עתידית תשכח. הכיסוי רחב בכוונה: 55 נטילות שומר ב־lxHandle.pas ו־37 ב־lxHandleX.pas נכון לאצווה שהציגה את הליבה
המשטח המושען משתרע על ערכי תאים ועיצוב תאים, TXLSWorkbook.Open, העתקה והדבקה, שמות מוגדרים (Add, שינוי שם, RefersTo, Visible, IsMacro, Comment, Delete), מטא־נתוני גיליון כמו Name, Zoom, Visible, StandardHeight, FreezePanes, Protect ו־Activate, הגדרת עמוד, מעברי עמוד, ו־Calculate. המיקום הוא כל העניין: השומר נלקח לפני שהשדה הראשון נכתב, לא נבדק אחר כך על ידי הוק התראה, כך שמוטציה שנדחתה משאירה את המודל זהה בית־אחר־בית. סוויטת הרגרסיה טוענת בדיוק זאת, וקוראת מחדש שם גיליון, זום, נראות, גובה תקני, שוליים, כיוון ומנייני מעברי עמוד אחרי כל קריאה שנדחתה. נתיבי הטעינה מקבלים את אותו טיפול שכבה אחת מטה, שם שער קריאת ה־ZIP מתאם ניפוח מקבילי עבור פורמטי החבילה
procedure TXLSWorksheet.Activate;
var
WriteGuard: IXLSWorkbookWriteGuard;
begin
// נלקח לפני שהשדה הראשון נוגע, לעולם לא אחרי
WriteGuard := FWorkbook._AcquireWriteGuard;
if not FSelected then
begin
FWorkbook.FWorkSheets.Deselect;
FSelected := True;
end;
FWorkbook.FWorkSheets.FActiveSheet := Self;
// רק שומר חיצוני ביותר שהושלם מקדם את הדור
WriteGuard.Complete;
end;למה כתיבה מקוננת מקדמת את הדור רק פעם אחת?
כי עסקת כתיבה מוגדרת על ידי השומר החיצוני ביותר בתהליכון, לא על ידי כל שומר בנפרד. הליבה מחזיקה מצב כותב לכל תהליכון שמחזיק מזהה תהליכון, עומק ודגל השלמה. AcquireWriteGuard שני באותו תהליכון מוצא את המצב הזה ומגדיל את Depth במקום ליצור עסקה חדשה, ורק כשה־Depth חוזר לאפס — כאשר השומר החיצוני ביותר סומן Complete — ה־FGeneration מתקדם. זה מה שמאפשר לפעולה ברמה גבוהה כמו Calculate או Open לקרוא לעשרה פרימיטיבים מוגנים מתחתיה ועדיין להירשם כשינוי אחד. קריאות Complete פנימיות נרשמות אבל אינן מזיזות את המניין בעצמן, ואפשר לשחרר את השומרים שלא בסדר בלי לשבש את החשבונאות
כיוון הכשל מפורש באותה מידה. אם שומר משוחרר בלי Complete — התוצאה הרגילה של חריגה שמפרקת את הפניית הממשק — הדור אינו מתקדם, כי עסקת הכתיבה מעולם לא תבעה הצלחה. הביטו בעיניים פקוחות מה זה אומר: HotXLS אינו מבצע rollback לעריכה החלקית. המניין רושם שאף עסקה מוצלחת לא הושלמה, וזה בדיוק האות שמטמון צריך, אבל שחזור המודל למצבו הקודם אינו משהו ששומר עם מניין הפניות יכול לעשות עבורכם. אם כשל באמצע עסקה עלול להשאיר את החוברת בצורה שאינכם יכולים לשחרר, שמרו על קובץ המקור ופתחו אותו מחדש, במקום לסמוך על האובייקט בזיכרון
מה מונה הדורות משתלם לכם
זיהוי התיישנות זול בלי סריקה. Generation הוא UInt64 שמתחיל ב־1 ומדלג על 0 בגלישה, כך ש־0 לעולם אינו ערך שהליבה מנפיקה והוא משמש סנטינל אמין של "מעולם לא נצפה". שני אינווריאנטים הופכים אותו לשימושי: הדור אינו יכול לזוז בזמן שקיים חוזה קריאה כלשהו, וכל עסקת כתיבה מוצלחת מגדילה אותו בדיוק פעם אחת. כך IXLSWorkbookReadLease.Generation הוא תצלום שנשאר קבוע לכל חיי החוזה, ו־IXLSWorkbookWriteGuard.StartGeneration אומר לכותב כיצד המודל נראה כשהעסקה שלו נפתחה. רשת, תצוגה מקדימה של הדפסה או מדד נגזר יכולים להשוות מספר שלם אחד במקום לבצע diff של שורות
var
Lease: IXLSWorkbookReadLease;
begin
Lease := FWorkbook._AcquireReadLease;
if Lease.Generation <> FCachedGeneration then
begin
FCachedGeneration := Lease.Generation;
RebuildRowHeightCache;
end;
PaintVisibleRows;
// FCachedGeneration מתחיל ב־0, ערך שהליבה לעולם לא מנפיקה,
// ולכן המעבר הראשון תמיד בונה מחדש
end;מה התיאום הזה אינו מבטיח
שלושה גבולות שווה לומר במישרין, כי להניח אחרת זו הדרך שבה המנגנון מנוצל לרעה. ראשית, שומר כתיבה אינו הדרה הדדית בין כותבים: הליבה מודרת קוראים כנגד כותבים, ושני תהליכונים שונים יכולים כל אחד להחזיק שומר כתיבה בו־זמנית, כל אחד מקדם את הדור באופן עצמאי — בדיקת רגרסיה טוענת בדיוק התנהגות זו. סידור תהליכוני הכותבים שלכם בטור נשאר עדיין בעבודתכם. שנית, שום דבר כאן אינו מנעול קובץ או מיוטקס חוצה תהליכים; הוא מתאם תהליכונים בתוך תהליך אחד כנגד מופע חוברת אחד, ושני תהליכים שפותחים את אותו .xlsx אינם יודעים דבר זה על זה. שלישית, הערובה מגיעה רק למתקשרים שבאמת נוטלים חוזה — קריאה בלי חוזה עדיין הולכת בנתיב חם ללא מנעול, שהוא מהיר וחסר הגנה לחלוטין. זו ליבת תיאום, לא מסד נתונים טרנזקציוני
בשימוש בתחומי הגבולות האלה זהו פרימיטיב קטן וכן: תשע בדיקות רגרסיה ייעודיות מכסות קוראים מרובים, את שני כיווני ההתנגשות, חזרניות, שחרור שלא בסדר, עסקאות שנזנחו, ומירוצי קריאה/כתיבה וכתיבה/כתיבה חוצי תהליכונים, בתוך סוויטה של 1,328 בדיקות שעוברת ב־Win32 וב־Win64. שלבו אותו עם נתיב השמירה המדורג לקובץ זמני בטוח־מפני קריסה וייצוא רקע הופך לדבר שאפשר לחשוב עליו מקצה לקצה — עקבי בזמן שהוא קורא, אטומי כשהוא כותב. חוזי קריאה, שומרי כתיבה ומונה הדורות מגיעים כחלק מהמנועים הקלאסי והחבילה ב־HotXLS Delphi Component עבור Delphi ו־C++Builder, ללא צורך בהגדרה כלשהי כדי להפעילם