PDFium no es thread-safe a nivel de módulo, así que dos instancias de TPdf trabajando sobre dos archivos distintos en dos hilos pueden corromperse entre sí igual. PDFium Component para Delphi lo maneja de dos maneras: desde v3.125.1, ValidatePdfFilesParallel serializa cada llamada nativa a PDFium detrás de un candado de todo el proceso, mientras que TPdf.RenderPagesParallel le da a cada worker su propia copia aislada del módulo PDFium. El bug que forzó la corrección era del peor tipo de intermitente. Una prueba de validación por lotes pasaba la mayoría de las veces, luego reportaba como fallido uno de dos archivos buenos, luego crasheaba la siguiente prueba en el mismo proceso con un access violation, y a veces tiraba abajo todo el runner con un código de salida en vez de un stack trace. Nada estaba mal en la prueba, y nada estaba mal en ningún documento suelto. El supuesto estaba mal: un TPdf por hilo no es aislamiento
¿Por qué no basta un TPdf por hilo?
Un TPdf por hilo no basta porque PDFium guarda su estado inseguro en el módulo, no en el documento. Cada TPdf es dueño de su propio handle FPDF_DOCUMENT, pero cada handle del proceso lo atiende la misma DLL cargada, y esa DLL carga singletons de proceso: la caché de fuentes, el page module, y otras estructuras globales que la carga de documentos, el parseo y el renderizado tocan todos. Dos hilos cargando dos archivos sin relación son dos hilos escribiendo en la misma caché de fuentes al mismo tiempo. Nadie es dueño de esos datos del lado Delphi, así que nada del lado Delphi puede bloquearlos por documento
El componente sí tiene un candado, y es fácil sacar la conclusión equivocada de él. TPdf envuelve sus propias rutas de render en una sección crítica interna (EnterRenderLock / LeaveRenderLock, métodos privados de TPdf). Ese candado es por instancia. Evita que dos hilos manejen el mismo TPdf a la vez, que es un peligro real, pero no puede ver una segunda instancia en otro hilo, así que la concurrencia entre instancias pasa de largo directamente. La regla general es simple de enunciar en una línea: en un solo módulo PDFium cargado, a lo sumo un hilo puede estar dentro de PDFium en cualquier momento, sin importar cuántos documentos estén abiertos
¿Cómo se ve la corrupción entre documentos en un proceso Delphi?
La corrupción entre documentos se ve como una mezcla aleatoria de fallos sin relación, y el daño sobrevive al código que lo causó. Antes de v3.125.1, ValidatePdfFilesParallel creaba un TPdf por hilo worker y corría Active := True más la construcción del reporte preflight concurrentemente sobre el módulo compartido. Los síntomas vistos en builds Delphi y Free Pascal por igual cubrían todo el rango:
- Un archivo válido falla al cargar, o vuelve del lote como fallido cuando debía pasar
- Un access violation asoma en una llamada posterior sin relación, a menudo en otra prueba u otro documento
External exception C000001Daparece en Delphi. Ese código esSTATUS_ILLEGAL_INSTRUCTION, levantado por la instrucciónud2que los macros internosCHECKyIMMEDIATE_CRASHde PDFium ejecutan cuando se rompe un invariante- El proceso sale con
0xC0000409(fail-fast, reportado como stack buffer overrun) o0xC0000374(corrupción de heap), sin ninguna excepción Delphi
Los últimos dos puntos son la razón de que el bug fuera tan difícil de clavar. La validación paralela terminaba, el estado global corrupto se quedaba atrás, y el siguiente fixture en el mismo proceso se tropezaba con él. En una corrida de regresión Delphi Win64, una oleada de fallos C000001D golpeó pruebas que jamás tocaron la validación por lotes; eran simplemente el primer código en usar PDFium después del daño. Los números medidos dejan la escala clara. Una sonda Delphi que corría la misma muestra por dos workers falló 122 de 160 documentos en una corrida y 138 de 160 en otra, y una de esas corridas levantó External exception C000001D de plano. Un caso de estrés de 8 documentos, 4 workers y 5 rondas falló o crasheó en 5 de 5 corridas en Free Pascal Win64. Después de la corrección, la misma sonda falló 0 de 1,200 documentos
Cómo ValidatePdfFilesParallel se mantiene seguro desde v3.125.1
ValidatePdfFilesParallel ahora serializa la mitad nativa de cada trabajo y mantiene la mitad administrada en paralelo. Cada worker toma una sección crítica a nivel de unidad antes de crear su TPdf, y la conserva a través de FileName, Active := True, la construcción del reporte preflight, y Free. La creación y la destrucción están dentro del candado a propósito: cerrar un documento vuelve a llamar al módulo igual que cargarlo. Una vez que el worker tiene capturado un record TPdfPreflightReport, suelta el candado y evalúa las reglas de validación contra ese record, lo cual no toca estado de PDFium, así que la evaluación de reglas de un archivo se solapa con el trabajo PDFium del siguiente
Dos cambios menores vinieron con la corrección. Un fallo de carga ahora lanza EPdfError con LastLoadReport.ErrorMessage, así que el ErrorMessage del ítem nombra el problema real de parseo en vez de un error secundario de "no active document". Y el costo se enuncia con honestidad: la parte PDFium del lote ahora es serial, así que en un lote dominado por parseo y preflight, workers extra compran poco. Si está en una versión anterior a v3.125.1, ponga WorkerCount en 1; eso elimina la concurrencia y con ella la corrupción
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 = conteo de procesadores, acotado a 8
Options.Standards := [ppsPdfA];
// Con un registry explícito, escoja usted el perfil que corresponda.
// Una lista Profiles vacía corre cada regla registrada, y las reglas de
// estándares que usted no preflighteó reportan "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;
Pasar nil como registry es la ruta corta: ValidatePdfFilesParallel entonces crea el registry por defecto él mismo, deriva la lista de perfiles de Options.Standards, y libera el registry al retornar. Los resultados siempre vuelven en orden de entrada, sea cual sea el orden en que terminaron los workers. Para los formatos de reporte y el wrapper de línea de comandos sobre el mismo engine, vea reportes preflight PDF por lotes con el CLI de PDFium Component, y para qué cubren los chequeos PDF/A en sí, validación preflight PDF/A en Delphi
¿Cómo corre RenderPagesParallel páginas de verdad en paralelo?
TPdf.RenderPagesParallel corre en paralelo porque sus workers jamás comparten un módulo PDFium. El método primero guarda el documento activo en un almacén de origen en el hilo que llama. Cada worker después copia la DLL PDFium cargada a un archivo de nombre único en el directorio temporal, carga esa copia con LoadLibrary, y la inicializa. Windows trata una DLL cargada desde una ruta distinta como un módulo distinto, así que cada copia recibe sus propios globales: su propia caché de fuentes, su propio page module, su propio todo. El worker abre el documento guardado en su módulo privado, renderiza sus páginas progresivamente con chequeos de cancelación entre pasos, luego destruye la librería, descarga la copia y borra el archivo
El aislamiento no es gratis, y los valores por defecto lo reflejan. Cada worker paga una copia de la DLL en disco, un segundo juego de globales PDFium en memoria, y un parseo fresco del documento. MaxWorkers = 0 significa a lo sumo 4 workers, MaxPixelsPerPage y MaxTotalOutputBytes acotan la salida cruda, y las opciones de render invertido y duotono nocturno se rechazan porque los buffers se devuelven crudos. El resultado es un TPdfParallelRenderReport cuyo array Results guarda un buffer de 32 bits top-down por página pedida, en orden de petición
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; // los números de página son base 1
Options := TPdfParallelRenderOptions.Default;
Options.Dpi := 150;
Options.MaxWorkers := 4;
// El snapshot de origen se toma en el módulo compartido, así que tome el
// candado PDFium de proceso si otros hilos también usan 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 el candado alrededor de la llamada. Los módulos de los workers son privados, pero el paso de snapshot al comienzo corre SaveAs sobre el módulo compartido desde el hilo que llama. Si nada más en su proceso toca TPdf concurrentemente puede quitar el candado; si algo lo toca, el snapshot necesita la misma protección que cualquier otra llamada al módulo compartido
| Patrón | Seguro entre documentos | El trabajo PDFium corre en paralelo | Costo |
|---|---|---|---|
Un TPdf por hilo, sin candado compartido | No | Sí, hasta que corrompe | Crashes intermitentes, estado de proceso dañado |
| Un candado de proceso alrededor de todas las llamadas PDFium | Sí | No | La parte PDFium es serial |
ValidatePdfFilesParallel desde v3.125.1 | Sí | No; la evaluación de reglas es paralela | El parseo y el preflight son seriales |
TPdf.RenderPagesParallel | Sí | Sí | Copia de DLL, memoria y un parseo fresco por worker |
¿Cómo debe estructurar su propio código PDFium multihilo?
Sus propios hilos deben compartir un candado de todo el proceso y conservarlo durante toda la vida de cada TPdf que usen, o bien usar una API del componente que aisle el módulo por usted. El candado tiene que ser un solo objeto para todo el proceso, no uno por hilo, por formulario o por documento; un candado que dos hilos no comparten no protege nada. El patrón de abajo refleja lo que el componente hace internamente desde v3.125.1: crear, cargar, leer y liberar dentro del candado, y luego hacer todo lo que no toca PDFium fuera de él
uses
System.Classes, System.SysUtils, System.SyncObjs, PDFium;
var
PdfiumLock: TCriticalSection; // un candado para todo el proceso
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; // cerrar el documento también es trabajo PDFium
end;
finally
PdfiumLock.Release;
end;
// Nada de PDFium bajo esta línea, así que esta parte corre en paralelo
FText := Raw.ToString.Trim;
finally
Raw.Free;
end;
end;
initialization
PdfiumLock := TCriticalSection.Create;
finalization
PdfiumLock.Free;
Unas cuantas reglas mantienen honesto el patrón en una aplicación real:
- Ponga
TPdf.CreateyFreedentro del candado, no solo las llamadas obvias. Cargar, cerrar, lecturas de propiedades comoPageCount, cambios de página, extracción de texto, renderizado y guardado todos alcanzan al módulo - Verifique
Activedespués de asignarlo. Una carga fallida dejaActiveenFalse, yLastLoadReport.ErrorMessagedice por qué - Sostenga el candado por documento en vez de por llamada. Un bloqueo más fino es posible en principio, pero solo si ningún miembro de
TPdfcorre jamás fuera de él, y la versión gruesa es la que el propio componente usa - Mantenga el trabajo lento que no sea PDFium, como escrituras a base de datos, indexado y llamadas de red, fuera del candado, o un solo consumidor lento serializará todo
- No trate el candado de render privado por instancia como un sustituto. Protege un
TPdfcontra sí mismo y nada más
La misma cautela aplica al código que usted no escribió como hilos crudos. Los futures en segundo plano son una buena manera de mantener los renders largos fuera del hilo de UI, como se describe en render PDF en segundo plano con futures cancelables, pero el executor de futures no agrega un candado global de PDFium propio. Si varios futures pueden manejar distintas instancias de TPdf al mismo tiempo, tome el mismo candado de proceso dentro de cada worker, y trate un visor en el hilo principal como un cliente más del módulo compartido. El uso entre instancias por las APIs asíncronas no ha sido auditado por separado, así que el supuesto conservador es que necesita la misma serialización que los hilos escritos a mano. Cuando necesite paralelismo PDFium real para algo que no sea renderizar páginas, procesos worker separados le dan a cada trabajo su propio módulo por construcción
Referencia rápida: reglas de hilos de PDFium para Delphi
- El estado inseguro de PDFium es de todo el módulo: la caché de fuentes, el page module y otros globales son compartidos por cada documento del proceso
- Un
TPdfpor hilo no aísla nada; dos instancias en dos hilos pueden corromperse entre sí igual - Los síntomas típicos son fallos de carga, access violations en código posterior,
External exception C000001D, y salidas con0xC0000409o0xC0000374 - La corrupción persiste en el proceso, así que la llamada que falla a menudo no es la que la causó
ValidatePdfFilesParalleles seguro desde v3.125.1; en versiones anteriores useWorkerCount := 1TPdf.RenderPagesParalleles genuinamente paralelo porque cada worker carga una copia aislada del módulo PDFium- Sus propios hilos, tareas y futures necesitan un candado de proceso que cubra cada
TPdfdeCreateaFree
PDFium Component envuelve el engine PDFium para Delphi con preflight y validación por lotes, renderizado paralelo aislado, trabajo en segundo plano cancelable y diagnósticos de carga detallados. Detalles y ediciones están en la página de producto de PDFium Component