كان خيط خلفي يصدّر تقريرا من 40,000 صف حين ضبط خيط الواجهة خلية واحدة، فكان الملف الذي حطّ على القرص لا يطابق أي مصنف وُجد إطلاقا. يعالج HotXLS تلك الفئة من الأخطاء في lxWorkbookView.pas، حيث يُصدر IXLSWorkbookViewCore عقود إيجار قراءة بتكلفة O(1) وحراس كتابة سريعي الفشل: ما دام عقد مفتوحا، فإن كل نقطة دخول للتعديل تثير استثناء بدلا من الكتابة
الفشل الذي يصل بلا أثر مكدس
قراءة مصنف ليست أبدا عملية ذرية واحدة. اجتياز تقرير هو عشرات آلاف القراءات الفردية للخلايا موزعة على ثوانٍ، وSetValue واحدة تحط بين اثنتين منها تكفي لتغيير ما يراه بقية الاجتياز. المحرك الكلاسيكي يجعل هذا ملموسا: TXLSCellRef.SetValue يمكنه استدعاء FSST.Remove لإسقاط مدخل سلسلة مشتركة، وإعادة ضبط FValueType، وإبطال حالة ذاكرة صيغة مؤقتة، كل ذلك بينما خيط آخر في منتصف إزاحة مراجع تلك البنى نفسها. لا شيء يتعطل في الحال. تحصل على تقرير مجاميعه الفرعية لا تتطابق، أو تصدير يقرأ بهدوء فهرس سلسلة صار يشير الآن إلى مكان آخر
لا يحل HotXLS هذا عمدا بجعل الكتّاب ينتظرون. يمكن لقارئ أن يمسك مصنفا لعدة ثوانٍ، وفي تطبيق VCL غالبا ما يكون الكاتب رد نداء لواجهة أو معالج حدث على الخيط الرئيسي — وحجب ذلك الخيط حتى ينتهي تصدير خلفي نتيجة أسوأ من إخفاق التحرير. لذا يثير قلب التنسيق 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 لا يتراجع عن التحرير الجزئي. العدّاد يسجل أنه لم تكتمل أي معاملة ناجحة، وهو بالضبط الإشارة التي تحتاجها ذاكرة مؤقتة، لكن استعادة النموذج إلى حالته السابقة ليس شيئا يستطيع حارس بعدّاد مراجع فعله نيابة عنك. وإن كان فشل في منتصف المعاملة يمكن أن يترك المصنف في شكل لا يمكنك شحنه، فاحتفظ بالملف المصدر وأعد فتحه، بدلا من الوثوق بالكائن في الذاكرة
ماذا يمنحك إياه عدّاد الجيل
كشف تقادم رخيص بلا أي مسح. Generation قيمة من نوع UInt64 تبدأ من 1 وتتخطى الصفر عند الالتفاف، فالصفر ليس قيمة يصدرها القلب إطلاقا ويعمل كقيمة حراسة موثوقة لـ«لوحظ قط». ثابتان يجعلانها قابلة للاستخدام: الجيل لا يمكنه التحرك ما دام أي عقد إيجار قراءة قائما، وكل معاملة كتابة ناجحة تزيده مرة واحدة بالضبط. لذا IXLSWorkbookReadLease.Generation لقطة تبقى ثابتة طوال عمر العقد، وIXLSWorkbookWriteGuard.StartGeneration تخبر الكاتب كيف كان النموذج حين فُتحت معاملته. شبكة عرض أو معاينة طباعة أو فهرس مشتق يمكنها مقارنة عدد صحيح واحد بدلا من مطابقة فروق الصفوف
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 لـDelphi وC++Builder، دون أي إعداد لازم لتفعيلها