Artikel Teknis

Read Lease dan Write Guard HotXLS untuk Workbook Delphi

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

Matriks koordinasi HotXLS yang memperlihatkan read lease saling hidup berdampingan dengan bebas, penulisan yang dicoba terhadap lease terbuka melempar EXLSWorkbookWriteGuardUnavailable, lease yang diminta di dalam transaksi tulis melempar EXLSWorkbookReadLeaseUnavailable, dan dua thread penulis tak pernah saling mengeksklusi
Pembaca berdampingan dan penulis gagal cepat terhadap mereka, tetapi inti tak pernah mengeksklusi satu thread penulis dari yang lain

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

HotXLS membayar koordinasi di batas sebuah pemindaian: critical section hanya menutupi akuisisi lease, pelepasan, dan batas transaksi tulis, sementara write guard diakuisisi di dalam TXLSCellRef.SetValue sehingga setiap API kenyamanan di atasnya digerbangkan sekali
Satu akuisisi dan satu pelepasan menutupi pemindaian lima puluh ribu sel, dan satu guard di dalam TXLSCellRef.SetValue menutupi setiap jalur tulis publik di atasnya
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 CompleteFGeneration 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

Dua lini masa transaksi tulis HotXLS dibandingkan: guard bersarang di satu thread menaikkan kedalaman dan maju counter generasi hanya ketika guard terluar selesai, sementara exception yang mengurai guard tanpa Complete menyisakan generasi tak berubah dan edit parsial di tempatnya
Kedalaman melacak bersarangnya, tetapi hanya transaksi terluar yang selesai memajukan generasi, dan yang digugurkan menyisakan counter dan edit parsial persis di tempatnya

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