O PDFium não é thread-safe a nível de módulo, então duas instâncias de TPdf trabalhando em dois arquivos diferentes em duas threads ainda podem corromper uma à outra. O PDFium Component para Delphi lida com isso de duas maneiras: desde a v3.125.1, o ValidatePdfFilesParallel serializa toda chamada nativa do PDFium atrás de um lock de processo inteiro, enquanto o TPdf.RenderPagesParallel dá a cada worker a própria cópia isolada do módulo PDFium. O bug que forçou o conserto era do pior tipo de intermitente. Um teste de validação em lote passava na maior parte do tempo, depois reportava um de dois arquivos bons como falho, depois crashava o próximo teste no mesmo processo com um access violation, e às vezes derrubava o runner inteiro com um exit code em vez de um stack trace. Nada estava errado com o teste, e nada estava errado com documento nenhum. A suposição estava errada: um TPdf por thread não é isolamento
Por que um TPdf por thread não basta?
Um TPdf por thread não basta porque o PDFium mantém o estado inseguro dele no módulo, não no documento. Cada TPdf possui o próprio handle FPDF_DOCUMENT, mas todo handle no processo é servido pela mesma DLL carregada, e essa DLL guarda singletons de processo: o font cache, o page module, e outras estruturas globais que carregamento, parsing e renderização de documentos todos tocam. Duas threads carregando dois arquivos sem relação são duas threads escrevendo no mesmo font cache ao mesmo tempo. Ninguém é dono desses dados do lado Delphi, então nada do lado Delphi pode travá-los por documento
O componente tem um lock, e é fácil tirar a conclusão errada dele. O TPdf embrulha os próprios caminhos de render dele numa critical section interna (EnterRenderLock / LeaveRenderLock, métodos privados do TPdf). Esse lock é por instância. Ele impede que duas threads dirijam o mesmo TPdf ao mesmo tempo, o que é um perigo real, mas ele não vê uma segunda instância noutra thread, então concorrência entre instâncias passa direto por ele. A regra geral é simples o bastante para caber numa linha: num único módulo PDFium carregado, no máximo uma thread pode estar dentro do PDFium a qualquer momento, independente de quantos documentos estejam abertos
Como é a corrupção entre documentos num processo Delphi?
A corrupção entre documentos parece uma mistura aleatória de falhas sem relação, e o dano sobrevive ao código que o causou. Antes da v3.125.1, o ValidatePdfFilesParallel criava um TPdf por thread de worker e rodava Active := True mais a construção do relatório preflight concorrentemente no módulo compartilhado. Os sintomas vistos em builds Delphi e Free Pascal cobriram o espectro inteiro:
- Um arquivo válido falha ao carregar, ou volta do lote como falho quando deveria passar
- Um access violation aparece numa chamada posterior sem relação, muitas vezes num teste diferente ou num documento diferente
External exception C000001Daparece no Delphi. Esse código éSTATUS_ILLEGAL_INSTRUCTION, levantado pela instruçãoud2que os macrosCHECKeIMMEDIATE_CRASHinternos do PDFium executam quando um invariante quebra- O processo sai com
0xC0000409(fail-fast, reportado como stack buffer overrun) ou0xC0000374(heap corruption), sem exceção Delphi nenhuma
Os dois últimos pontos são o motivo de o bug ser tão difícil de prender. A validação paralela terminava, o estado global corrompido ficava para trás, e o próximo fixture no mesmo processo tropeçava nele. Numa rodada de regressão Delphi Win64, uma onda de falhas C000001D atingiu testes que nunca tocaram em validação em lote; eles eram simplesmente o primeiro código a usar o PDFium depois do dano. Os números medidos tornam a escala clara. Uma probe Delphi que rodava a mesma amostra por dois workers falhou em 122 de 160 documentos numa rodada e 138 de 160 noutra, e uma dessas rodadas levantou External exception C000001D na lata. Um caso de estresse de 8 documentos, 4 workers e 5 rodadas falhou ou crashou em 5 de 5 rodadas no Free Pascal Win64. Depois do conserto, a mesma probe falhou em 0 de 1.200 documentos
Como o ValidatePdfFilesParallel permanece seguro desde a v3.125.1
O ValidatePdfFilesParallel agora serializa a metade nativa de cada trabalho e mantém a metade gerenciada paralela. Todo worker toma uma critical section a nível de unit antes de criar o seu TPdf, e a segura através de FileName, Active := True, a construção do relatório preflight, e Free. Criação e destruição ficam dentro do lock de propósito: fechar um documento chama de volta para o módulo tanto quanto carregar. Uma vez que o worker tem um record TPdfPreflightReport capturado, ele libera o lock e avalia as regras de validação contra esse record, o que não toca em estado PDFium nenhum, então a avaliação de regras de um arquivo se sobrepõe ao trabalho PDFium do próximo
Duas mudanças menores vieram com o conserto. Uma falha de carregamento agora lança EPdfError com LastLoadReport.ErrorMessage, então o ErrorMessage do item nomeia o problema real de parse em vez de um erro secundário "sem documento ativo". E o custo é dito com honestidade: a parte PDFium do lote agora é serial, então num lote dominado por parsing e preflight, workers extras compram pouco. Se você está numa versão anterior à v3.125.1, defina WorkerCount como 1; isso remove a concorrência e a corrupção junto
uses
System.SysUtils, PDFium, FPdfPreflightReport;
procedure ValidateBatch(const Files: array of string);
var
Registry: TPdfValidationRuleRegistry;
Options: TPdfBatchValidationOptions;
Report: TPdfBatchValidationReport;
I: Integer;
begin
Registry := CreateDefaultPdfValidationRuleRegistry;
try
Options := TPdfBatchValidationOptions.Default;
Options.WorkerCount := 4; // 0 = contagem de processadores, limitado a 8
Options.Standards := [ppsPdfA];
// Com um registry explícito, selecione você mesmo o perfil correspondente.
// Uma lista Profiles vazia roda toda regra registrada, e regras de
// standards que você não pré-checou reportam "did not pass"
SetLength(Options.ValidationOptions.Profiles, 1);
Options.ValidationOptions.Profiles[0] := 'PDF/A';
Report := ValidatePdfFilesParallel(Files, Registry, Options);
finally
Registry.Free;
end;
for I := 0 to High(Report.Results) do
case Report.Results[I].Status of
pbvisPass: Writeln('PASS ', Report.Results[I].FileName);
pbvisFail: Writeln('FAIL ', Report.Results[I].FileName);
pbvisError: Writeln('ERROR ', Report.Results[I].FileName, ': ',
Report.Results[I].ErrorMessage);
else
Writeln('SKIP ', Report.Results[I].FileName); // pbvisCancelled
end;
Writeln(Report.PassedDocumentCount, ' passed, ',
Report.FailedDocumentCount, ' failed, ',
Report.ErrorDocumentCount, ' errors');
end;
Passar nil como registry é o caminho mais curto: o ValidatePdfFilesParallel então cria o registry padrão ele mesmo, deriva a lista de perfis do Options.Standards, e libera o registry quando retorna. Os resultados sempre voltam na ordem de entrada, seja qual for a ordem em que os workers terminaram. Para os formatos de relatório e o wrapper de linha de comando em volta do mesmo motor, veja relatórios de preflight PDF em lote com a CLI do PDFium Component, e para o que as checagens PDF/A em si cobrem, validação de preflight PDF/A em Delphi
Como o RenderPagesParallel roda páginas de verdade em paralelo?
O TPdf.RenderPagesParallel roda em paralelo porque os workers dele nunca compartilham um módulo PDFium. O método primeiro salva o documento ativo numa store de origem na thread chamadora. Cada worker então copia a DLL PDFium carregada para um arquivo de nome único no diretório temp, carrega essa cópia com LoadLibrary, e a inicializa. O Windows trata uma DLL carregada de um caminho diferente como um módulo diferente, então cada cópia ganha os próprios globals: o próprio font cache, o próprio page module, o próprio tudo. O worker abre o documento salvo no módulo privado dele, renderiza as páginas dele progressivamente com checagens de cancelamento entre passos, depois destrói a biblioteca, descarrega a cópia e apaga o arquivo
O isolamento não é de graça, e os padrões refletem isso. Cada worker paga por uma cópia da DLL no disco, um segundo conjunto de globals do PDFium na memória, e um parse novo do documento. MaxWorkers = 0 significa no máximo 4 workers, MaxPixelsPerPage e MaxTotalOutputBytes limitam a saída crua, e as opções de render invertido e duotone noturno são rejeitadas porque os buffers voltam crus. O resultado é um TPdfParallelRenderReport cujo array Results guarda um buffer top-down de 32 bits por página pedida, na ordem do pedido
procedure RenderAllPages(Pdf: TPdf);
var
Options: TPdfParallelRenderOptions;
Report: TPdfParallelRenderReport;
Pages: array of Integer;
I: Integer;
begin
SetLength(Pages, Pdf.PageCount);
for I := 0 to High(Pages) do
Pages[I] := I + 1; // números de página são base um
Options := TPdfParallelRenderOptions.Default;
Options.Dpi := 150;
Options.MaxWorkers := 4;
// O snapshot de origem é tirado no módulo compartilhado, então segure o
// lock PDFium de todo o processo se outras threads também usam TPdf
PdfiumLock.Acquire;
try
Report := Pdf.RenderPagesParallel(Pages, Options);
finally
PdfiumLock.Release;
end;
for I := 0 to High(Report.Results) do
if Report.Results[I].Status = pprsSucceeded then
SavePageBuffer(Report.Results[I]) // Width, Height, Stride, PixelFormat, Pixels
else
Writeln('Page ', Report.Results[I].PageNumber, ': ',
Report.Results[I].ErrorMessage);
end;
Note o lock em volta da chamada. Os módulos dos workers são privados, mas o passo de snapshot no começo roda o SaveAs no módulo compartilhado a partir da thread chamadora. Se nada mais no seu processo toca em TPdf concorrentemente você pode dispensar o lock; se algo toca, o snapshot precisa da mesma proteção que qualquer outra chamada de módulo compartilhado
| Padrão | Seguro entre documentos | Trabalho do PDFium roda em paralelo | Custo |
|---|---|---|---|
Um TPdf por thread, sem lock compartilhado | Não | Sim, até corromper | Crashes intermitentes, estado do processo danificado |
| Um lock de processo inteiro em volta de todas as chamadas PDFium | Sim | Não | A parte do PDFium é serial |
ValidatePdfFilesParallel desde a v3.125.1 | Sim | Não; a avaliação de regras é paralela | Parsing e preflight são seriais |
TPdf.RenderPagesParallel | Sim | Sim | Cópia da DLL, memória e um parse novo por worker |
Como estruturar o seu próprio código PDFium multithread?
As suas próprias threads devem compartilhar um lock de processo inteiro e segurá-lo por toda a vida de cada TPdf que usarem, ou então usar uma API do componente que isole o módulo para você. O lock tem de ser um único objeto para o processo todo, não um por thread, por form ou por documento; um lock que duas threads não compartilham não protege nada. O padrão abaixo espelha o que o componente faz internamente desde a v3.125.1: criar, carregar, ler e liberar dentro do lock, então fazer tudo que não toca no PDFium fora dele
uses
System.Classes, System.SysUtils, System.SyncObjs, PDFium;
var
PdfiumLock: TCriticalSection; // um lock para o processo inteiro
type
TTextExtractThread = class(TThread)
private
FFileName: string;
FText: string;
protected
procedure Execute; override;
public
constructor Create(const AFileName: string);
property ExtractedText: string read FText;
end;
constructor TTextExtractThread.Create(const AFileName: string);
begin
inherited Create(True);
FFileName := AFileName;
end;
procedure TTextExtractThread.Execute;
var
Pdf: TPdf;
Page: Integer;
Raw: TStringBuilder;
begin
Raw := TStringBuilder.Create;
try
PdfiumLock.Acquire;
try
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FFileName;
Pdf.Active := True;
if not Pdf.Active then
raise EPdfError.Create(Pdf.LastLoadReport.ErrorMessage);
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
Raw.AppendLine(Pdf.Text);
end;
finally
Pdf.Free; // fechar o documento também é trabalho do PDFium
end;
finally
PdfiumLock.Release;
end;
// Nada de PDFium abaixo desta linha, então esta parte roda em paralelo
FText := Raw.ToString.Trim;
finally
Raw.Free;
end;
end;
initialization
PdfiumLock := TCriticalSection.Create;
finalization
PdfiumLock.Free;
Algumas regras mantêm o padrão honesto numa aplicação real:
- Ponha o
TPdf.Createe oFreedentro do lock, não só as chamadas óbvias. Carregar, fechar, leituras de propriedades comoPageCount, mudanças de página, extração de texto, renderização e salvar todos alcançam o módulo - Confira o
Activedepois de atribuí-lo. Um carregamento que falhou deixa oActiveemFalse, e oLastLoadReport.ErrorMessagediz por quê - Segure o lock por documento em vez de por chamada. Locking mais fino é possível em princípio, mas só se nenhum membro de
TPdfrodar jamais fora dele, e a versão grossa é a que o próprio componente usa - Mantenha trabalho lento não PDFium, como gravações em banco de dados, indexação e chamadas de rede, fora do lock, ou um consumidor lento serializa tudo
- Não trate o lock de render privado por instância como substituto. Ele guarda um
TPdfcontra ele mesmo e nada mais
A mesma cautela vale para código que você não escreveu como threads cruas. Background futures são um bom jeito de manter renders longos fora da thread de UI, como descrito em renderização PDF em background com futures canceláveis, mas o executor de futures não adiciona um lock PDFium global próprio. Se vários futures podem dirigir instâncias de TPdf diferentes ao mesmo tempo, tome o mesmo lock de processo inteiro dentro de cada worker, e trate um viewer na thread principal como mais um cliente do módulo compartilhado. Uso entre instâncias pelas APIs assíncronas não foi auditado separadamente, então a suposição conservadora é de que ele precisa da mesma serialização que threads escritas à mão. Quando você precisa de paralelismo PDFium de verdade para algo além da renderização de páginas, processos worker separados dão a cada trabalho o próprio módulo dele por construção
Referência rápida: regras de threading do PDFium para Delphi
- O estado inseguro do PDFium vale para o módulo inteiro: font cache, page module e outros globals são compartilhados por todo documento do processo
- Um
TPdfpor thread não isola nada; duas instâncias em duas threads ainda podem corromper uma à outra - Os sintomas típicos são falhas de carregamento, access violations em código posterior,
External exception C000001D, e saídas com0xC0000409ou0xC0000374 - A corrupção persiste no processo, então a chamada que falha muitas vezes não é a que a causou
- O
ValidatePdfFilesParallelé seguro desde a v3.125.1; em versões anteriores useWorkerCount := 1 - O
TPdf.RenderPagesParallelé paralelo de verdade porque cada worker carrega uma cópia isolada do módulo PDFium - As suas próprias threads, tasks e futures precisam de um lock de processo inteiro cobrindo cada
TPdfdoCreateaoFree
O PDFium Component embrulha o motor PDFium para Delphi com preflight e validação em lote, renderização paralela isolada, trabalho em background cancelável e diagnósticos de carregamento detalhados. Detalhes e edições estão na página de produto do PDFium Component