Teknik Makale

Delphi'de Eşzamanlı ZIP Inflate: HotXLS Okuma Kapısı

İ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

Delphi'de sahnelemeli bir XLSX açma yolunun şeması; A aşaması, B aşaması sayfaları paralel ayrıştırmadan önce her şişmiş çalışma sayfası XML'ini belleğe kopyalar
A aşaması her şişmiş çalışma sayfasını tek iş parçacığında özel bir bellek akışına kopyalar; böylece sekiz dolu sayfalı bir çalışma kitabı zirvede sekiz büyük tampon tutar

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

Delphi'de HotXLS okuma kapısının mimari şeması; eşzamanlı çalışma sayfası işçileri özel bayt aralıklarını şişirirken tek bir critical section yalnızca paylaşılan akışın seek ve okumasını sıralar
Her işçi alt akış konumuna ve z_stream'ine sahiptir; paylaşılan tek durum paket imlecidir ve TZipReadGate, konumlu tek bir okuma için kilidi tutar

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

Delphi'de HotXLS işçi akış zincirinin şeması; paylaşılan paketten okuma kapısından doğrulanmış akışa uzanır ve boyut ile CRC32 kontrollerini ateşleyen boşaltma döngüsünü gösterir
Boşaltma döngüsü konumu bildirilen girdi boyutuna iter; böylece boyut ve CRC32 karşılaştırması çalışabilir ve bir XML okuyucusunun okumadan bırakacağı bozulmayı yakalar

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