Delphi ve C++Builder için yerel Excel bileşen kütüphanesi olan HotXLS, tek bir açık ZIP paketinden birkaç XLSX çalışma sayfasını aynı anda inflate eder. Mekanizma, lxZipArchive.pas'da küçük bir sınıf olan, paket akışını artı bir kritik bölümü tutan ve tam olarak bir yöntem sunan TZipReadGate'dir. Seek-ve-oku çiftini sıralar. O çiftin üzerindeki her şey eşzamanlı olarak çalışır
Bu tasarımı zorunlu kılan problem, büyük bir çalışma kitabı açmış her Delphi geliştiricisinin karşılaştığı bir problemdir. 80 MB'lık bir xlsx, 80 MB deflate edilmiş XML'dir ve içindeki çalışma sayfası parçaları kabaca beş ila on kat genişler. Açma yolunuz her çalışma sayfasını ayrıştırmadan önce bir bellek akışına çıkarıyorsa, inşa ettiğiniz çalışma kitabının üzerine inflate edilmiş baytlar için ödeme yaparsınız ve doruk, tek bir hücre oluşturulmadan önce gelir. Bu makale, o hazırlık adımını ortadan kaldıran paket düzeyi eşzamanlılık hakkındadır. Üzerinde oturan ayırıcı tavanı, paralel XLSX ayrıştırma ve bellek yöneticisi üzerine makalede ele alınmıştır ve bir-kez-oku, asla-gerçekleştirme API'si akış tabanlı doğrudan okuyucu anlatımında ele alınmıştır
Eski açma yolu neden her çalışma sayfasını RAM'de hazırlıyordu
HotXLS'deki orijinal paralel açma, üç aşamalı bir boru hattıydı ve orta aşama worker'lar üzerinde çalışan tek aşamaydı. Aşama A, sayfa listesini sıralı olarak dolaştı, her çalışma sayfasını oluşturdu, ilişki parçasını okudu ve tüm inflate edilmiş çalışma sayfası XML'ini özel bir TMemoryStream'e kopyaladı. Aşama B, ParseWorksheetXml'i havuz üzerinde yaydı. Aşama C, küçük uydu parçalar için çağıran iş parçacığında arşive geri döndü: yorumlar, iş parçacıklı yorumlar, çizimler, grafikler, tablolar. Bu şekil, belirtilen bir nedenle seçildi. lxParallelParse.pas'daki başlık yorumu, zip arşivinin ve inflate durumunun iş parçacığı güvenli olmadığını açık sözlerle söylüyordu ve dahili notlar daha ileri gitti: arşivi kilitlemekle uğraşmayın, çünkü inflate durum makinesi girdi başına sıraya konduğunda kilit hiçbir şey kazandırmaz. Aşama A, her arşiv dokunuşunu bir iş parçacığında tutmak için vardı. Maliyet, sekiz meşgul sayfalı bir çalışma kitabının, bellekte aynı anda sekiz tamamen inflate edilmiş çalışma sayfası XML arabelleği tutmasıydı ve bu arabellekler tüm açma yolundaki en büyük geçici nesnelerdir
İki iş parçacığı tek bir ZIP akışından inflate edebilir mi?
Evet, ve eski yargı belirli, bulunabilir bir şekilde yanlıştı: iki farklı durum parçasını tek bir cümlede birleştirdi. Inflate durumu gerçekten paylaşılamaz. Bir zlib z_stream, tek bir sıkıştırılmış üye için kayan pencereyi, Huffman tablolarını ve bit konumunu taşır ve aynısının içinden bayt iten iki iş parçacığı çöp üretir. Altta yatan bayt kaynağı tamamen farklı bir sorudur ve oradaki cevap, bir dosya akışının korunmaya değer tam olarak bir değişebilir paylaşılan durum parçasına sahip olduğudur, konum imleci
ZIP konteyneri ayrımı yasal kılar. Bir ZIP arşivindeki her üye bağımsız olarak sıkıştırılır: kendi yerel dosya başlığı, kendi DataOffset'inde kendi deflate bit akışı, merkezi dizinde kendi CRC32'si ve boyutları. Sağlam bir 7z bloğunun sahip olduğu gibi üyeler arasında yayılan paylaşılan bir sözlük yoktur, dolayısıyla girdi N, girdi M'ye dokunmadan inflate edilebilir. Her worker'a kendi bayt aralığı üzerinde kendi z_stream'ini verin ve çarpıştıkları tek şey seek'tir. Bu çarpışma TZipReadGate'in kaldırdığı şeydir ve tüm sınıf tek bir ekranda okunacak kadar kısadır
type
TZipReadGate = class
private
FBaseStream: TStream;
FLock: TRTLCriticalSection;
public
constructor Create(ABaseStream: TStream);
destructor Destroy; override;
function ReadAt(AOffset: Int64; var Buffer; Count: Longint): Longint;
end;
function TZipReadGate.ReadAt(AOffset: Int64; var Buffer;
Count: Longint): Longint;
begin
if Count <= 0 then
begin
Result := 0;
Exit;
end;
EnterCriticalSection(FLock);
try
FBaseStream.Position := AOffset;
Result := FBaseStream.Read(Buffer, Count);
finally
LeaveCriticalSection(FLock);
end;
end;
TZipReadGate neyi korur ve neyi kasıtlı olarak korumaz
TZipReadGate.ReadAt, tek bir bölünmez işlemi, paylaşılan akışı konumlandırmayı ve ondan okumayı korur, başka hiçbir şeyi değil. TZipArchive.OpenArchive, merkezi dizin başarıyla ayrıştırıldıktan sonra kapıyı FInputStream üzerinde bir kez inşa eder ve TZipArchive.Close onu serbest bırakır. Yazma için açılan arşivler asla bir tane almaz. Bir worker'ın paket üzerinde gerçekleştirdiği her okuma bu yüzden bir okuma boyunca tutulan tek bir kritik bölüme akar
Geri kalan her şey kilidin dışında kalır çünkü zaten özel veya zaten değişmezdir. TZipSubStream kendi FPosition'ını tutar, dolayısıyla her worker kendi girdisindeki kendi yerini izler. TZipEntry.GetStream'in o alt akış üzerinde inşa ettiği TZLibStream, girdi başınadır, ham deflate için -15'lik windowBits ile oluşturulur ve asla paylaşılmaz. Merkezi dizin, her yerel başlık dahil olmak üzere, herhangi bir worker başlamadan önce tamamen ayrıştırılır, dolayısıyla GetEntryByName, eşzamanlılık başladığında salt-okunur bir hash araması hâline gelmiştir. Yönlendirmenin kendisi TZipSubStream.Read'de üç satırdır ve kapısız dal, her mevcut tek-iş-parçacıklı çağıranı eski kod yolunda tutan şeydir
function TZipSubStream.Read(var buffer; Count: longint): longint;
var
rest: Int64;
rc: longint;
begin
rest := FSize - FPosition;
if (Count > rest) then
Count := rest;
if FReadGate <> nil then
rc := FReadGate.ReadAt(FOffset + FPosition, buffer, Count)
else
begin
FBaseStream.Position := FOffset + FPosition;
rc := FBaseStream.Read(buffer, Count);
end;
FPosition := FPosition + rc;
Result := rc;
end;
Kapı, çekişme altında ne kadara mal olur?
"Arşiv üzerinde küresel kilit" ifadesinin önerdiğinden daha az, çünkü TZLibStream'in kullandığı ayrıntılık düzeyi var. Girdi arabelleği, $4000 olarak tanımlanan BufferSize'dır, dolayısıyla ReadInputBuffer her yeniden doldurmada 16 KB sıkıştırılmış bayt çeker ve bunları zng_inflate'e verir. Bir kilit edinimi bu yüzden 16 KB deflate girdisini kapsar, ki bu çalışma sayfası XML'i için worker'ın sonra hiçbir şey tutmadan çözüp ayrıştırdığı yaklaşık 100 KB markup'a genişler. Kilit, işletim sistemi önbelleğine karşı konumlandırılmış bir okuma için tutulur; kapıladığı iş milisaniyeler cinsinden ölçülür
Dürüst sınır, o oranın tersine döndüğü yerdir. Deflate edilmek yerine saklanan girdiler, gizleyecek gecikmeyi saklayacak hiçbir inflate işi olmadan kapıdan birebir okur, dolayısıyla saklanan üyelerle dolu bir paket çok daha sert sıralanır. Yavaş ortamda soğuk bir dosya kritik bölümü genişletir, çünkü içindeki okuma artık bir önbellek isabeti değil gerçek bir disk aktarımıdır. Ve bir avuç worker'ı geçince kapı zaten ilk çarpacağınız şey değildir: çalışma sayfası ayrıştırma tahsis-ağırlıklıdır ve Delphi bellek yöneticisi, okuma kapısı kısıtlama hâline gelmeden çok önce tahsisleri iş parçacıkları boyunca sıraya koyar. TXLSXWorkbook.ParallelParseThreads'in çekirdek başına bir iş parçacığı yerine varsayılan olarak otomatik bir tavana sahip olmasının nedeni budur
Worker gövdesi ve unutulması kolay boşaltma döngüsü
Kapı yerinde olduğunda, HotXLS Aşama A hazırlığını tamamen sildi. Worker artık kendi girdi akışını açar ve onu doğrudan ayrıştırıcıya besler. İki geçici alan girdileri taşır: FParZip, paralel aşama süresince arşivi tutar, FParSheetPartNames parça adlarını tutar ve her ikisi de finally bloğunda temizlenir, dolayısıyla başarısız bir açılıştan eskimiş bir işaretçi hayatta kalmaz. TZipArchive.OpenFile'dan geri gelen akış, bir TZipSubStream'i saran bir TZLibStream'i saran bir TZipVerifiedStream'dir ve dıştakini serbest bırakmak zinciri serbest bırakır
procedure TXLSXWorkbook.ParseSheetJob(AIndex: Integer);
var
Stream: TStream;
DrainBuffer: array [0..32767] of Byte;
PartName: WideString;
begin
PartName := WideString(FParSheetPartNames[AIndex]);
Stream := FParZip.OpenFile(PartName);
if Stream = nil then
Exit;
try
ParseWorksheetXml(Stream, FSheets.ByPos[AIndex], FParSst,
FParRels[AIndex], FParFontMap, FParFillMap, FParBorderMap,
FParNumFmtMap, FParAlignMap, FParProtMap, FParDateMap);
// Consume any trailing bytes so the ZIP entry size and CRC are verified.
while Stream.Read(DrainBuffer, SizeOf(DrainBuffer)) > 0 do
;
finally
Stream.Free;
end;
end;
Boşaltma döngüsü, eski kodun düz bir taşımasının düşüreceği ayrıntıdır ve onu düşürmek sessizce bütünlük kontrolünü devre dışı bırakır. TZipVerifiedStream, baytlar geçerken çalışan bir CRC32 biriktirir ve VerifyComplete'i yalnızca konumu merkezi dizinde kayıtlı sıkıştırılmamış boyuta ulaştığında çağırır; boyut uyuşmazlığı ve CRC32 uyuşmazlığı istisnaları buradan gelir, artı bildirilenden daha uzun bir girdiyi yakalayan bir baytlık bir yoklama okuması. Bir XML okuyucu kapanış elemanında durur ve genellikle bir yeni satır veya birkaç bayt sondaki boşluğu okunmadan bırakır, dolayısıyla boşaltma olmadan konum asla bildirilen boyuta ulaşmaz ve kontroller asla tetiklenmez. Kalanı bir taslak arabelleğe okumak hiçbir şeye mal olmaz ve onları geri getirir. Hazırlık akışları var olduğunda, XlsxCopyStreamAll bunu tesadüfen yapıyordu
Hâlâ sıralı çalışan ve hepsini kapatan bayrak
Aşama A, çıkarma olmadan hayatta kalır. Hâlâ her çalışma sayfasını oluşturur ve ilişkilerini çağıran iş parçacığında okur, ki worker'lar başladığında her paylaşılan haritayı değişmez bırakan şey budur. Aşama C hâlâ ondan sonra sayfaları sıralı olarak yorumlar, çizimler, grafikler ve tablolar için dolaşır ve koruması, eski hazırlık dizisindeki bir null kontrolünden parça adına karşı zip.Exists'e değişti. Worker'ların dokunduğu paylaşılan salt-okunur girdiler, paylaşılan dize tablosu ve cellXf haritaları, Aşama B başlamadan önce tamamdır ve onun sırasında asla yazılmaz
var
Wb: TXLSXWorkbook;
begin
Wb := TXLSXWorkbook.Create;
try
Wb.ParallelParse := True; // default; False forces one sheet at a time
Wb.ParallelParseThreads := 4; // 0 selects the automatic cap
Wb.Open('quarterly-consolidation.xlsx');
// ... workbook is identical either way ...
finally
Wb.Free;
end;
end;
ParallelParse'i Open'dan önce False'a ayarlamak, aynı iş prosedürünü bir iş parçacığı sayısıyla gönderir ve RunParallelJobs, çağıran iş parçacığında düz bir döngüye dejenere olur. Bu, iki nedenle bilinmeye değerdir: sahada bir iş parçacığı endişesi ortaya çıkarsa tek satırlık cevaptır ve sıralı ile paralel yolların ayrılmak yerine tek bir ayrıştırma kodu gövdesini paylaştığı anlamına gelir. Worker istisnaları yakalanır, en düşük iş indeksi kazanır ve hata, her worker katıldıktan sonra çağıran iş parçacığında yeniden fırlatılır, dolayısıyla bozuk bir çalışma sayfası hâlâ beklenen yerde tek bir istisna olarak yüzeye çıkar. Çevredeki açma yolunun genel ayarı Delphi'de büyük çalışma kitabı performansı kılavuzunda ele alınmıştır
Burada anlatılan okuma kapısı, paralel açma aşaması ve akış tabanlı girdi erişimi, tam kaynakla, Delphi ve C++Builder için standart HotXLS Excel bileşeni'nin bir parçası olarak gönderilir; ürün sayfası, paralel açma özellikleri dahil tam TXLSXWorkbook referansını taşır