Фонова нишка експортираше отчет с 40,000 реда, когато нишката на интерфейса зададе една клетка, и файлът, който кацна на диска, не съответстваше на никоя работна книга, която някога е съществувала. HotXLS обработва този клас бъгове в lxWorkbookView.pas, където IXLSWorkbookViewCore издава O(1) read lease и fail-fast write guard: докато lease е отворен, всяка входна точка за мутация хвърля изключение вместо да пише
Провалът, който идва без stack trace
Четенето на работна книга никога не е една атомарна операция. Обхождането на отчет е десетки хиляди отделни четения на клетки, разпределени върху секунди, и едно-единствено SetValue, кацнало между две от тях, стига да промени това, което остатъкът от обхождането вижда. Класическият двигател прави това конкретно: TXLSCellRef.SetValue може да извика FSST.Remove, за да премахне споделен низов запис, да нулира FValueType и да анулира състояние на кеша на формули — всичко това, докато друга нишка е по средата на дереференциране на точно тези структури. Нищо не се срива на място. Получавате отчет, чиито междинни тотали не се събират, или експорт, който тихо прочита низов индекс, който вече сочи другаде
HotXLS нарочно не решава това, като кара писателите да чакат. Четец може да държи работна книга няколко секунди, а във VCL приложение писателят често е callback от интерфейса или обработчик на събитие в главната нишка — блокирането на тази нишка, докато фонов експорт приключи, е по-лош изход от провала на редакцията. Затова координационното ядро хвърля EXLSWorkbookWriteGuardUnavailable в момента, в който срещу отворен lease се опита запис, преди да е пипнато едно-единствено поле, и извикващият решава дали да постави редакцията в опашка, да опита отново или да уведоми потребителя. Fail-fast конфликти, а не такива в опашка
Безопасно ли е четенето на работна книга от две нишки?
Да, при условие че двамата четци държат lease и никой не пише. IXLSWorkbookViewCore.AcquireReadLease взима TCriticalSection, увеличава брояч, записва моментна снимка на текущата генерация и връща IXLSWorkbookReadLease — постоянно време независимо дали работната книга държи хиляда клетки или милион. Произволен брой lease съществуват едновременно, те могат да се освобождават в произволен ред и всеки държи ядрото живо чрез собствената си интерфейсна препратка, така че lease, който надживява обекта, създал го, е безопасен, а не висяща препратка. И двата двигателя участват: TXLSWorkbook в lxHandle.pas и TXLSXWorkbook в lxHandleX.pas всеки изгражда ядро в конструктора си и излагат _AcquireReadLease и _AcquireWriteGuard
Не по-малко важно е и това, което lease не добавя към пътя на четене. Критичната секция покрива придобиването на lease, освобождаването на lease и границите на транзакциите за запис — нищо друго. Обикновеното четене на клетка никога не влиза в заключване, монитор или атомарен брояч, така че държането на lease струва едно придобиване и едно освобождаване за цялото сканиране, а не по едно на клетка. Това е същият дизайнерски инстинкт зад работата по паралелния XLSX парсинг и алокатора на памет: плащай за координация на границата, никога във вътрешния цикъл. Симетричното правило също важи — AcquireReadLease хвърля EXLSWorkbookReadLeaseUnavailable винаги когато WriteDepth е различен от нула, така че не можете да отворите lease отвътре в транзакция за запис, дори на нишката, която пише
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;
// Lease излиза от обхвата тук: броячът му на препратки пада до нула,
// ReleaseReadLease се изпълнява и писателите отново стават възможни
end;
Къде всъщност седи write guard?
В най-ниския изменяем слой, никога в convenience API-то над него. _AcquireWriteGuard се извиква отвътре в самия TXLSCellRef.SetValue, което означава, че всеки публичен път, който се влива в него — Range.Value, присвояване на текст на работен лист, копиране клетка по клетка, поставяне — се огражда веднъж, вместо всеки wrapper да повтаря проверка, която бъдещ wrapper ще забрави. Покритието е нарочно широко: 55 придобивания на guard в lxHandle.pas и 37 в lxHandleX.pas към партидата, която въведе ядрото
Оградената повърхност обхваща стойности и формати на клетки, TXLSWorkbook.Open, копиране и поставяне, дефинирани имена (Add, преименуване, RefersTo, Visible, IsMacro, Comment, Delete), метаданни на работен лист като Name, Zoom, Visible, StandardHeight, FreezePanes, Protect и Activate, настройка на страница, прекъсвания на страница и Calculate. Мястото е важната част: guard се придобива преди първото поле да е записано, а не се валидира след това от notification hook, така че отхвърлена мутация оставя модела байт-идентичен. Регресионният набор твърди точно това, като препрочита име на лист, мащаб, видимост, стандартна височина, полета, ориентация и брой прекъсвания на страница след всеки отказан извикване. Пътищата на зареждане получават същото отношение един слой по-надолу, където ZIP read gate координира конкурентен inflate за пакетните формати
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;
// Само завършен най-външен guard придвижва генерацията
WriteGuard.Complete;
end;
Защо вложен запис придвижва генерацията само веднъж?
Защото транзакцията за запис се дефинира от най-външния guard на нишка, а не от всеки guard поотделно. Ядрото пази състояние на писател за нишка, съдържащо id на нишка, дълбочина и флаг за завършване. Второ AcquireWriteGuard на същата нишка намира това състояние и увеличава Depth, вместо да създава нова транзакция, и само когато Depth падне обратно до нула — при най-външния guard белязан като Complete — FGeneration се придвижва. Това позволява операция от високо ниво като Calculate или Open да извика десет оградени примитива отдолу и пак да се регистрира като една промяна. Вътрешните извиквания на Complete се записват, но не местват брояча сами, а guard-ите могат да се освобождават в произволен ред, без да чупят отчитането
Посоката на провала е еднакво изрична. Ако guard бъде освободен без Complete — обичайната последица от изключение, което развива интерфейсната препратка — генерацията не се придвижва, защото транзакцията за запис никога не заяви успех. Бъдете ясни какво означава това: HotXLS не връща частичната редакция обратно. Броячът записва, че никаква успешна транзакция не е завършила, което е точно сигналът, от който един кеш се нуждае, но възстановяването на модела до предишното му състояние не е нещо, което guard с броене на препратки може да направи вместо вас. Ако провал по средата на транзакция може да остави работната книга във форма, която не можете да доставите, запазете изходния файл и го отворете наново, вместо да се доверите на обекта в паметта
Какво ви носи генерационният брояч
Евтино откриване на остарялост без сканиране. Generation е UInt64, който започва от 1 и прескача 0 при препълване, така че 0 никога не е стойност, която ядрото издава, и работи като надежден сантинел „никога не наблюдавано“. Два инварианта я правят използваема: генерацията не може да се движи, докато съществува който и да е read lease, и всяка успешна транзакция за запис я увеличава точно веднъж. Така IXLSWorkbookReadLease.Generation е моментна снимка, която остава константна за целия живот на lease, а IXLSWorkbookWriteGuard.StartGeneration казва на писателя как е изглеждал моделът, когато неговата транзакция се е отворила. Мрежа, преглед за печат или производен индекс може да сравни едно цяло число, вместо да сравнява ред по ред
var
Lease: IXLSWorkbookReadLease;
begin
Lease := FWorkbook._AcquireReadLease;
if Lease.Generation <> FCachedGeneration then
begin
FCachedGeneration := Lease.Generation;
RebuildRowHeightCache;
end;
PaintVisibleRows;
// FCachedGeneration започва от 0, стойност, която ядрото никога не издава,
// така че много първото минаване винаги преизгражда
end;
Какво това координиране не обещава
Три ограничения си заслужава да се кажат ясно, защото допускането на обратното е начинът този механизъм да бъде злоупотребен. Първо, write guard не е взаимно изключване между писатели: ядрото изключва четци срещу писатели, а две различни нишки могат да държат write guard едновременно, като всяка придвижва генерацията независимо — регресионен тест твърди точно това поведение. Сериализирането на собствените ви нишки-писатели все пак е ваша работа. Второ, нищо тук не е заключване на файл или между-процесен мутекс; то координира нишки в един процес срещу един екземпляр на работна книга, а два процеса, отварящи един и същ .xlsx, не знаят нищо един за друг. Трето, гарантията достига само извикващи, които действително взимат lease — четене без lease пак върви по отключен hot path, който е бърз и изцяло незащитен. Това е координационно ядро, а не транзакционна база данни
Използван в тези граници това е малък, честен примитив: девет специализирани регресионни теста покриват множество четци, двете посоки на конфликт, реентерантност, освобождаване в произволен ред, прекъснати транзакции и надпревари четене/запис и запис/запис между нишки, в рамките на набор от 1,328 теста, минаващи на Win32 и Win64. Комбинирайте го с пътя за запис чрез стадии на временни файлове, устойчив на сривове, и фоновият експорт става нещо, за което можете да разсъждавате от край до край — консистентен, докато чете, атомарен, когато пише. Read lease, write guard и генерационният брояч се доставят като част от класическия и пакетния двигател в HotXLS Delphi Component за Delphi и C++Builder, без да е нужна конфигурация, за да ги включите