İki iş parçacığı bir .xlsx paketi üzerinden tek bir TFileStream paylaşır. A iş parçacığı Position'u sheet1.xml'in ilk sıkıştırılmış baytına ayarlar; B iş parçacığı biraz sonra onu sheet2.xml'in ilk baytına ayarlar ve A'nın Read'i öteki üyeye ait bir tampon döndürür. A'nın şişiricisi artık başkasının deflate akışının ortasını çözüyor; zlib genellikle birkaç blok sonra Z_DATA_ERROR ile durur ve tesadüfen çıktı üretse üyenin CRC32'si tutmaz. Suçlu deflate değil. Yarış büsbütün akışın konum imlecinde yaşar ve seek-okuma çiftinin çevresine bir kilit, birçok işçinin aynı paketin üyelerini aynı anda şişirmesine yeter
Bunu istemenin nedeni bellektir. Diyelim bir çalışma kitabı diskte 80 MB ve çalışma sayfası XML'i şişince beş-on kat büyüyor. Her çalışma sayfasını ayrıştırmadan önce bir tampona çıkaran bir açma yolu, kurduğu çalışma kitabının üstüne o şişmiş baytları da öder ve zirve, tek bir hücre var olmadan önce gelir. Bu makale, çıkarma adımını ortadan kaldıran paket düzeyindeki eşzamanlılığı işler. Üstündeki tahsisçi tavanını paralel XLSX ayrıştırma ve bellek yöneticisi makalesi, hiç çalışma kitabı kurmayan tek-okuma API'sini ise akışlı doğrudan okuyucu anlatımı işler
Sahneleme tasarımı ve bellekte ne tuttuğu
Bir XLSX açılışını paralelleştirmenin ihtiyatlı yolu, yalnızca orta aşaması işçilerde çalışan üç aşamalı bir hattır. A aşaması çağıran iş parçacığında sayfa listesini gezdirir, her çalışma sayfasını oluşturur, ilişki bölümünü okur ve şişmiş çalışma sayfası XML'inin tamamını özel bir TMemoryStream'e kopyalar. B aşaması XML ayrıştırmayı bir iş parçacığı havuzuna yayar. C aşaması yine çağıran iş parçacığında arşive döner ve küçük uydu bölümleri alır: yorumlar, dizili yorumlar, çizimler, grafikler ve tablolar
O biçimin savunması sağlam görünür. Bir ZIP arşivi ve şişirme durumu iş parçacığı güvenli değildir; o halde arşive her dokunuşu tek iş parçacığında tutun, kilidi büsbütün atlayın. Fatura, sekiz dolu sayfalı bir çalışma kitabının aynı anda sekiz tam şişmiş çalışma sayfası tamponu tutmasıdır ve o tamponlar, açma yolundaki en büyük geçici nesnelerdir. Yukarıda varsayılan 80 MB'lık paket için bu, yalnızca bir ayrıştırıcının bir kez okuması diye var olan birkaç yüz megabayt XML metnidir
HotXLS, A ve C aşamalarını tutar ama kopyayı atar. B aşamasındaki her işçi paylaşılan paket üzerinde kendi giriş akışını açar ve onu doğrudan XML okuyucusuna verir; şişmiş çalışma sayfası hiçbir zaman tek blok olarak var olmaz. Bu yalnızca "iş parçacığı güvenli değil" argümanının aslında iki ayrı şeyden söz ettiği anlaşılınca mümkün olur
İki iş parçacığı tek bir ZIP akışından inflate edebilir mi?
Evet; her iş parçacığı kendi şişiricisine sahip olduğu ve paylaşılan bayt kaynağının okumada sıralandığı sürece. İhtiyatlı argüman iki tür durumu tek cümleye katıyor. Şişirme durumu gerçekten paylaşılamaz: bir zlib z_stream, RFC 1951'in tanımladığı 32 KB'lık kayan pencereyi, geçerli Huffman tablolarını ve tek bir sıkıştırılmış üyenin içindeki bit konumunu taşır; aynı olanın üzerinden bayt iten iki iş parçacığı çöp üretir. Bayt kaynağı ayrı bir konudur. Bir dosya akışının korumaya değer tam olarak tek değişken durumu vardır: konumu
ZIP kapsayıcısı bu ayrımı meşru kılar. PKWARE'nin APPNOTE'u (bölüm 4.3) paketi bağımsız üyeler olarak dizer; her üyenin kendi yerel dosya başlığı, kendi ofsetindeki kendi sıkıştırılmış verisi ve merkezi dizinde kayıtlı kendi CRC32'si ve boyutları vardır. Katı bir 7z bloğundaki gibi üyeler arasına span olan sözlük yoktur; N girdisi, M girdisinin tek bir baytını okumadan şişirilebilir. Her işçiye kendi bayt aralığı üzerinde özel bir şişirici verin; geriye kalan tek çarpışma seek'tir. HotXLS'te o çarpışma lxZipArchive.pas içindeki TZipReadGate tarafından halledilir ve tüm sınıf tek ekrana sığar
type
TZipReadGate = class
private
FBaseStream: TStream;
FLock: TXLSCriticalSection; // critical section üzerinden lxPlatform sarmalayıcısı
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;
XLSEnterCriticalSection(FLock);
try
FBaseStream.Position := AOffset;
Result := FBaseStream.Read(Buffer, Count);
finally
XLSLeaveCriticalSection(FLock);
end;
end;
Okuma kapısı neyi kilitler, neyi dışarıda bırakır
TZipReadGate.ReadAt tek bir bölünmez işlemi korur: paylaşılan akışı konumlandırmak ve ondan okumak. TZipArchive.OpenArchive, merkezi dizin ayrıştırıldıktan sonra hem dosya adı hem TStream aşırı yükleri için kapıyı bir kez kurar ve arşiv serbest bırakıldığında onu da bırakır. Yazmak için oluşturulan arşivler kapı almaz; çünkü onlardan kimse eşzamanlı okumaz
Gerisi, hâlihazırda özel ya da hâlihazırda donmuş olduğu için kilidin dışında kalır. TZipArchive.OpenFile'ın döndürdüğü her giriş akışı, kendi üyesi içinde kendi konumunu tutar ve onu yalnızca ReadAt çağırdığı anda mutlak paket ofsetine çevirir. Etrafına sarılan şişirici her girdi için, zlib'in zlib başlıksız ham deflate diye okuduğu -15 pencere boyuyla oluşturulur ve başka bir iş parçacığına asla verilmez. Merkezi dizin — yerel başlıklar dahil — hiçbir işçi başlamadan tamamen ayrıştırılır; eşzamanlılık başladığında ada göre girdi aramak salt okunur bir hash aramasıdır. Aramalar büyük-küçük harfe duyarsızdır ve .. bölümleri içeren adları reddeder
Kendi akışınızla TXLSXWorkbook.Open(Stream) çağırdığınızda bir sonuç önem taşır. Kapı o akışın sarılır ve kilidi, kütüphanenin işçilerini birbirleriyle koordine eder; sizin kodunuzdan habersizdir. Open çalışırken uygulamanızdaki başka bir iş parçacığı aynı TStream'i okur ya da yeniden konumlandırırsa ilk paragraftaki imleç yarışı geri döner; akışı teslim edin ve Open dönene kadar dokunmayın
Çekişme altında tek kilit ne kadara mal olur?
"Arşivde global bir kilit" deyişinin çağrıştırdığından azdır; çünkü her kilit alımı ne kadar işi kapsıyor. Şişirici girdisini 64 KB'lık sıkıştırılmış bayt parçalarıyla doldurur; kapıdan bir geçiş, bir işçiye 64 KB deflate verisi verir ve işçi hiçbir şey tutmadan onu çözüp ayrıştırır. Daha önce varsayılan beş-on kat genişlemeyle bu, kabaca 320-640 KB çalışma sayfası işaretlemesi olur. Kilidin kendisi, genellikle işletim sisteminin dosya önbelleğinden karşılanan konumlu bir okumayı kapsar
Oran üç yerde tersine döner; üçü de girdiyle ya da makineyle ilgilidir. Sıkıştırmasız saklanan üyeler, alımlar arasında şişirme işi olmadan her okuma çağrısında kapıdan geçer; çoğu yazıcı çalışma sayfası bölümlerini deflate eder ama her şeyi saklayan bir aracın ürettiği paket daha zor sıralanır. Yavaş ya da ağ depolamasındaki soğuk bir dosya kritik bölümü genişletir; içindeki okuma gerçek bir aktarıma dönüşer. Ve bir avuç işçinin ötesinde ilk duvar zaten kapı değildir: çalışma sayfası ayrıştırması yoğun tahsis yapar ve Delphi bellek yöneticisi, okuma kapısı doymadan çok önce iş parçacıkları arasında tahsisleri sıralar
Son nokta iş parçacığı sayısına karar verir. TXLSXWorkbook.ParallelParseThreads varsayılanı 0'dır; mantıksal işlemci başına bir işçi başlatır, asla sayfa sayısından çok değil. Dört-sekiz mantıksal işlemcili bir dizüstünde bu varsayılan iyidir. Onlarca sayfalı çalışma kitapları açan 32 iş parçacıklı bir build sunucusunda 4 gibi açık küçük bir tavan koyun: fazladan işçiler çoğunlukla tahsisçide kuyruğa girer ve her biri kendi şişiricisini ve tamponlarını tutar. Dosyalarınız için doğru sayıyı bulmak üzere özelliği 1, 2, 4 ve 8'e ayarlayıp temsili bir çalışma kitabını döngüde açın ve duvar süresini Task Manager ya da Process Explorer'daki zirve çalışma kümesiyle karşılaştırın
Bir girdiyi sonuna dek okumak CRC kontrolünü ateşleyen şeydir
Arşivin döndürdüğü her giriş akışı kendini kontrol eder ama yalnızca sonda. Baytlar akarken yürüyen bir CRC32 biriktirir; konumu merkezi dizinde kayıtlı sıkıştırılmamış boyuta ulaştığında iki değeri karşılaştırır ve bir bayt fazladan sonda arar. Başarısızlıklar sabit iletili özel durumlar olarak yüzeye çıkar: 'ZIP entry uncompressed size mismatch', 'ZIP entry CRC32 mismatch', 'ZIP entry exceeds its declared size'; erken tükenen üye için 'ZIP entry ended before its declared size'
Bir XML okuyucusu kapanan </worksheet> etiketinde durur. Sonrasındaki şey — çoğu zaman yalnızca sonda kalan bir satır sonu — okunmamış kalır; konum bildirilen boyuta hiç ulaşmaz ve karşılaştırma hiç çalışmaz. Sahneleme tasarımı o kontrolü bedavaya aldı; çünkü bir girdiyi tampona kopyalamak her baytı okur. Girdi akışını doğrudan okuyan bir ayrıştırıcı almaz; bu yüzden her HotXLS işçisi, akışı serbest bırakmadan önce ayrıştırıcının bıraktığını 32 KB'lık bir tamponla boşaltır ve en dıştaki akışı serbest bırakmak, altındaki şişiriciyi ve alt akışı da bırakır
Aynı kural, kendi kodunuz çalışma kitabı yükleyicisine vermeden önce gelen bir paketi sağlama almak için lxZipArchive'tan TZipArchive kullandığında da geçerlidir. Her bölümün ilk birkaç kilobaytını okuyup kök öğesini koklayan bir işlev bütünlük hakkında hiçbir şey kanıtlamaz. Sonuna dek okuyun:
uses
Classes, SysUtils, lxZipArchive;
// Bir paket bölümünü bildirilen sonuna dek okur; böylece boyut ve CRC32 kontrolleri çalışır.
// PartIsIntact sizin yazacağınız bir yardımcıdır; kütüphanenin parçası değildir
function PartIsIntact(Zip: TZipArchive; const PartName: WideString;
out Problem: string): Boolean;
var
Stream: TStream;
Buffer: array [0..32767] of Byte;
begin
Result := False;
Problem := '';
Stream := Zip.OpenFile(PartName); // bölüm yokken nil
if Stream = nil then
begin
Problem := 'part not found';
Exit;
end;
try
try
while Stream.Read(Buffer, SizeOf(Buffer)) > 0 do
;
Result := True;
except
on E: Exception do
Problem := E.Message; // ör. 'ZIP entry CRC32 mismatch'
end;
finally
Stream.Free;
end;
end;
var
Zip: TZipArchive;
Problem: string;
begin
Zip := TZipArchive.Create;
try
Zip.OpenArchive('quarterly-consolidation.xlsx');
if not PartIsIntact(Zip, 'xl/worksheets/sheet1.xml', Problem) then
Writeln('sheet1.xml: ', Problem);
finally
Zip.Free;
end;
end;
O koddaki ikinci tuzak geri sarmadır. Giriş akışları yalnızca ileri akar: Size okumak işler, çünkü bildirilen boyut baştan bilinir; ama bir göz atmadan sonra Position := 0 yapmak 'ZIP entry stream is forward-only' fırlatır. Bir bileşen ısrarla seek edecekse girdiyi OpenFile ile ikinci kez açın; ucuzdur ve sıfır ofsette taze bir akış verir. Okumak için açılmış bir arşiv üzerinde birden çok iş parçacığı her biri OpenFile çağırabilir — çalışma kitabı yükleyicisinin yaptığı tam olarak budur; akışları yaşarken kimsenin arşivi serbest bırakmaması koşuluyla
Hâlâ sıralı çalışanlar ve onları kapatan anahtar
A aşaması, çıkarma düşürülmüş olarak hâlâ çalışır. Her çalışma sayfasını oluşturur ve ilişkilerini çağıran iş parçacığında okur; işçiler başladığında paylaşılan her haritanın değişmez kalmasını sağlayan da budur. Paylaşılan dize tablosu ve hücre-biçim tabloları B aşaması başlamadan tamamlanır ve sırasında hiç yazılmaz. C aşaması sonrasında yorumlar, çizimler, grafikler ve tablolar için sayfaları sırayla gezdirir; bölümü pakette eksik olan sayfaları atlar
uses
SysUtils, lxHandleX;
var
Wb: TXLSXWorkbook;
begin
Wb := TXLSXWorkbook.Create;
try
Wb.ParallelParse := True; // varsayılan; False sayfaları tek tek ayrıştırır
Wb.ParallelParseThreads := 4; // 0 = mantıksal işlemci başına bir işçi
Wb.SelectedSheets.Add('Q3 Detail'); // isteğe bağlı: hücreleri yalnızca bu sayfa için ayrıştır
if Wb.Open('quarterly-consolidation.xlsx') <> 1 then
raise Exception.Create('Workbook open failed');
// yüklenen çalışma kitabı, hangi yol ayrıştırdıysa özdeştir
finally
Wb.Free;
end;
end;
Open'dan önce ParallelParse'i False yapmak aynı sayfa başına işi tek iş parçacığıyla çalıştırır ve havuz, çağıran iş parçacığında düz bir döngüye indirgenir. Tek çalışma sayfalı bir çalışma kitabı zaten o yolu izler. Bir açma sorununu teşhis ederken anahtarı aklınızda tutun: tek satırda iş parçacıklarını tablodan siler ve iki yol da aynı ayrıştırma kodunu çalıştırdığı için dönen çalışma kitabı bu ayara bağlı değildir
Hatalar da sıralı şekli korur. Bir işçi fırlattığında başka sayfa dağıtılmaz, tüm işçiler beklenir ve en düşük sayfa indeksinden gelen özel durum çağıran iş parçacığında yeniden fırlatılır; bozuk bir sheet3.xml yine de Open'dan tek bir özel durum olarak yüzeye çıkar. RecoveryMode True iken ayrıştıramayan bir çalışma sayfası atlanır ve çalışma kitabı teşhislerine uyarı olarak kaydedilir. SelectedSheets dışında kalan sayfalar giriş akışlarını hiç açmaz ve MetadataOnly paralel aşamayı büsbütün atlar. Açma yolunun daha geniş ayarı, Delphi'de büyük çalışma kitabı performansı kılavuzunda işlenir
Okuma kapısı, paralel açma aşaması ve burada gösterilen girdi düzeyi ZIP erişimi, Delphi ve C++Builder için tam kaynak kodla gelen HotXLS Delphi Excel component'in parçasıdır; ürün sayfasında anlatılan TXLSXWorkbook açma özelliklerini lxZipArchive.pas ile yan yana okuyabilirsiniz