Sebuah thread latar sedang mengekspor laporan 40.000 baris ketika thread UI mengatur satu sel, dan file yang mendarat di disk tak cocok dengan workbook mana pun yang pernah ada. HotXLS menangani kelas bug itu di lxWorkbookView.pas, tempat IXLSWorkbookViewCore menerbitkan read lease O(1) dan write guard fail-fast: selama sebuah lease terbuka, setiap titik masuk mutasi melempar alih-alih menulis
Kegagalan yang datang tanpa stack trace
Membaca sebuah workbook tak pernah satu operasi atomik. Satu penelusuran laporan adalah puluhan ribu pembacaan sel individual tersebar selama beberapa detik, dan satu SetValue yang mendarat di antara dua di antaranya cukup mengubah apa yang dilihat sisa penelusuran. Mesin klasik membuatnya konkret: TXLSCellRef.SetValue bisa memanggil FSST.Remove untuk membuang entri shared string, me-reset FValueType, dan membatalkan state cache formula, semuanya sementara thread lain sedang di tengah jalan me-dereferensikan persis struktur-struktur itu. Tak ada yang crash di tempat. Anda mendapat laporan yang subtotalnya tak menambah, atau ekspor yang diam-diam membaca indeks string yang kini menunjuk ke tempat lain
HotXLS sengaja tak menyelesaikannya dengan membiarkan penulis menunggu. Sebuah pembaca bisa memegang workbook selama beberapa detik, dan di aplikasi VCL penulisnya sering callback UI atau event handler di thread utama — memblokir thread itu sampai ekspor latar selesai adalah hasil yang lebih buruk daripada menggagalkan edit-nya. Jadi inti koordinasi melempar EXLSWorkbookWriteGuardUnavailable begitu sebuah penulisan dicoba terhadap lease yang terbuka, sebelum satu field pun tersentuh, dan pemanggil memutuskan apakah mengantre edit, mencoba lagi, atau memberi tahu pengguna. Konflik fail-fast, bukan yang diantrekan
Apakah workbook aman dibaca dari dua thread?
Ya, asalkan kedua pembaca memegang lease dan tak ada yang menulis. IXLSWorkbookViewCore.AcquireReadLease mengambil TCriticalSection, menaikkan counter, mengambil snapshot generasi saat ini, dan mengembalikan IXLSWorkbookReadLease — waktu konstan tak peduli workbook memegang seribu sel atau sejuta. Berapa pun lease bisa berdampingan, mereka boleh dilepas dalam urutan apa pun, dan masing-masing memin inti tetap hidup lewat referensi interfacenya sendiri, sehingga lease yang hidup lebih lama dari objek pembuatnya aman alih-alih pointer menggantung. Kedua mesin berpartisipasi: TXLSWorkbook di lxHandle.pas dan TXLSXWorkbook di lxHandleX.pas masing-masing membangun inti di konstruktor mereka dan mengekspos _AcquireReadLease serta _AcquireWriteGuard
Yang sama pentingnya adalah apa yang lease tidak tambahkan ke jalur baca. Critical section menutupi akuisisi lease, pelepasan lease, dan batas transaksi tulis — tak lebih. Pembacaan per-sel biasa tak pernah masuk lock, monitor, atau counter atomik, jadi memegang lease berbiaya satu akuisisi dan satu pelepasan untuk seluruh pemindaian, bukan satu per sel. Itu naluri desain yang sama di balik pekerjaan parsing XLSX paralel dan alokator memori: bayar koordinasi di batas, tak pernah di loop dalam. Aturan simetrisnya juga berlaku — AcquireReadLease melempar EXLSWorkbookReadLeaseUnavailable kapan pun WriteDepth non-nol, jadi Anda tak bisa membuka lease dari dalam transaksi tulis, bahkan di thread penulisnya sendiri
uses
lxHandle, lxWorkbookView;
procedure TReportThread.Execute;
var
Lease: IXLSWorkbookReadLease;
Sheet: TXLSWorksheet;
Row: Integer;
Total: Double;
begin
// Raises EXLSWorkbookReadLeaseUnavailable if a write is in flight
Lease := FWorkbook._AcquireReadLease;
Sheet := FWorkbook.Sheets[1];
Total := 0;
for Row := 1 to 50000 do
Total := Total + Sheet.Cells[Row, 3].Value;
FTotal := Total;
// Lease leaves scope here: its reference count drops to zero,
// ReleaseReadLease runs, and writers become possible again
end;
Di mana write guard sebenarnya duduk?
Di lapisan mutable terendah, tak pernah di API kenyamanan di atasnya. _AcquireWriteGuard dipanggil dari dalam TXLSCellRef.SetValue itu sendiri, yang berarti setiap jalur publik yang bermuara ke sana — Range.Value, penugasan teks worksheet, salin sel-per-sel, paste — digerbangkan sekali alih-alih setiap wrapper mengulang pemeriksaan yang wrapper masa depan akan lupakan. Cakupannya sengaja lebar: 55 akuisisi guard di lxHandle.pas dan 37 di lxHandleX.pas pada batch yang memperkenalkan inti itu
Permukaan yang digerbangkan membentang nilai sel dan pemformatan sel, TXLSWorkbook.Open, salin dan tempel, defined names (Add, ganti nama, RefersTo, Visible, IsMacro, Comment, Delete), metadata worksheet seperti Name, Zoom, Visible, StandardHeight, FreezePanes, Protect, dan Activate, page setup, page break, dan Calculate. Penempatan adalah intinya: guard diakuisisi sebelum field pertama ditulis, bukan divalidasi setelahnya lewat hook notifikasi, jadi mutasi yang ditolak menyisakan model byte-identik. Regresi suite mengasertikan persis itu, membaca ulang nama sheet, zoom, visibilitas, tinggi standar, margin, orientasi, dan hitungan page break setelah setiap panggilan yang ditolak. Jalur muat mendapat perlakuan yang sama satu lapis ke bawah, tempat gerbang baca ZIP mengoordinasikan inflate serentak untuk format paket
procedure TXLSWorksheet.Activate;
var
WriteGuard: IXLSWorkbookWriteGuard;
begin
// Acquired before the first field is touched, never after
WriteGuard := FWorkbook._AcquireWriteGuard;
if not FSelected then
begin
FWorkbook.FWorkSheets.Deselect;
FSelected := True;
end;
FWorkbook.FWorkSheets.FActiveSheet := Self;
// Only a completed outermost guard advances the generation
WriteGuard.Complete;
end;
Mengapa penulisan bersarang hanya maju generasi sekali?
Karena transaksi tulis didefinisikan oleh guard terluar di sebuah thread, bukan oleh masing-masing guard. Inti menyimpan state penulis per-thread yang memegang id thread, kedalaman, dan flag penyelesaian. AcquireWriteGuard kedua di thread yang sama menemukan state itu dan menaikkan Depth alih-alih membuat transaksi baru, dan hanya ketika Depth jatuh kembali ke nol — dengan guard terluar telah ditandai Complete — FGeneration maju. Inilah yang membiarkan operasi tingkat tinggi seperti Calculate atau Open memanggil sepuluh primitif tergerbangkan di bawahnya dan tetap tercatat sebagai satu perubahan. Panggilan Complete dalam dicatat tetapi tak memindahkan counter dengan sendirinya, dan guard boleh dilepas tak berurutan tanpa merusak pembukuan
Arah kegagalannya sama eksplisitnya. Jika sebuah guard dilepas tanpa Complete — konsekuensi biasa dari exception yang mengurai referensi interface — generasi tak maju, karena transaksi tulis tak pernah mengklaim sukses. Matangkan mata tentang artinya: HotXLS tak mengembalikan edit parsial. Counter mencatat bahwa tak ada transaksi sukses yang selesai, yang persis sinyal yang dibutuhkan cache, tetapi mengembalikan model ke keadaan sebelumnya bukan sesuatu yang bisa dilakukan guard berbasis reference-counting untuk Anda. Jika kegagalan di tengah transaksi bisa menyisakan workbook dalam bentuk yang tak bisa Anda kirim, simpan file sumbernya dan buka ulang, alih-alih mempercayai objek in-memory
Apa yang dibeli counter generasi untuk Anda
Deteksi kebasaan yang murah tanpa pemindaian. Generation adalah UInt64 yang mulai di 1 dan melompati 0 saat wraparound, jadi 0 tak pernah nilai yang ditulis inti dan bekerja sebagai sentinel "tak pernah teramati" yang andal. Dua invarian membuatnya terpakai: generasi tak bisa bergerak selama ada read lease, dan setiap transaksi tulis sukses menaikkannya tepat sekali. Jadi IXLSWorkbookReadLease.Generation adalah snapshot yang tetap konstan sepanjang hidup lease, dan IXLSWorkbookWriteGuard.StartGeneration memberi tahu penulis seperti apa bentuk model saat transaksinya dibuka. Sebuah grid, print preview, atau indeks turunan bisa membandingkan satu integer alih-alih mem-diff baris
var
Lease: IXLSWorkbookReadLease;
begin
Lease := FWorkbook._AcquireReadLease;
if Lease.Generation <> FCachedGeneration then
begin
FCachedGeneration := Lease.Generation;
RebuildRowHeightCache;
end;
PaintVisibleRows;
// FCachedGeneration starts at 0, a value the core never issues,
// so the very first pass always rebuilds
end;
Apa yang koordinasi ini tak janjikan
Tiga batas layak dinyatakan terus terang, karena mengasumsikan sebaliknya adalah cara mekanisme ini disalahgunakan. Pertama, write guard bukan eksklusi timbal balik antar penulis: inti mengeksklusi pembaca terhadap penulis, dan dua thread berbeda bisa masing-masing memegang write guard pada saat bersamaan, masing-masing memajukan generasi secara independen — satu tes regresi mengasertikan persis perilaku ini. Menserialisasi thread penulis Anda sendiri tetap pekerjaan Anda. Kedua, tak ada di sini yang merupakan file lock atau mutex lintas-proses; ia mengoordinasikan thread di dalam satu proses terhadap satu instance workbook, dan dua proses yang membuka .xlsx yang sama tak saling kenal. Ketiga, jaminan hanya menjangkau pemanggil yang benar-benar mengambil lease — pembacaan tanpa lease tetap menyusuri jalur panas tanpa kunci, yang cepat dan sepenuhnya tak terlindungi. Ini inti koordinasi, bukan basis data transaksional
Dipakai dalam batas-batas itu ia primitif kecil yang jujur: sembilan tes regresi khusus menutup banyak pembaca, kedua arah konflik, reentransi, pelepasan tak berurutan, transaksi yang digugurkan, serta ras baca/tulis dan tulis/tulis lintas-thread, di dalam suite 1.328 tes yang lulus di Win32 dan Win64. Pasangkan dengan jalur simpan bertahap tahan-crash lewat file temp dan ekspor latar menjadi sesuatu yang bisa Anda nalari dari ujung ke ujung — konsisten selagi membaca, atomik saat menulis. Read lease, write guard, dan counter generasi dikirim sebagai bagian mesin klasik dan paket di HotXLS Delphi Component untuk Delphi dan C++Builder, tanpa konfigurasi yang dibutuhkan untuk mengaktifkannya