PDFium no es thread-safe a nivel de módulo, así que dos instancias TPdf trabajando dos archivos distintos en dos hilos pueden seguir corrompiéndose entre sí. PDFium Component para Delphi lo maneja de dos maneras: desde v3.125.1, ValidatePdfFilesParallel serializa toda llamada nativa a PDFium detrás de un único lock de todo el proceso, mientras TPdf.RenderPagesParallel da a cada worker su propia copia aislada del módulo PDFium. El bug que forzó el arreglo era del peor tipo de intermitente. Una prueba de validación por lotes pasaba casi siempre, luego reportaba uno de dos archivos buenos como fallido, luego tumbaba la siguiente prueba del mismo proceso con un access violation, y a veces se llevaba por delante todo el runner con un código de salida en lugar de un stack trace. La prueba no tenía nada malo, y ningún documento individual tampoco. El supuesto era lo que fallaba: 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 posee su propio manejador FPDF_DOCUMENT, pero todo manejador del proceso lo sirve la misma DLL cargada, y esa DLL guarda singletons de todo el 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 a la vez. Nadie posee esos datos por el lado Delphi, así que nada por el lado Delphi puede bloquearlos por documento
El componente sí tiene un lock, y es fácil sacar de él la conclusión equivocada. TPdf envuelve sus propios caminos de render en una sección crítica interna (EnterRenderLock / LeaveRenderLock, métodos privados de TPdf). Ese lock 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 junto a él. La regla general es lo bastante simple para decirla en una línea: en un único módulo PDFium cargado, como mucho un hilo puede estar dentro de PDFium en cada 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 ejecutaba Active := True más la construcción del informe de preflight concurrentemente sobre el módulo compartido. Los síntomas vistos tanto en compilaciones Delphi como de Free Pascal 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 aflora en una llamada posterior sin relación, a menudo en una prueba distinta o un documento distinto
External exception C000001Daparece en Delphi. Ese código esSTATUS_ILLEGAL_INSTRUCTION, lanzado por la instrucciónud2que ejecutan las macros internasCHECKyIMMEDIATE_CRASHde PDFium cuando se rompe un invariante- El proceso sale con
0xC0000409(fail-fast, reportado como stack buffer overrun) o0xC0000374(corrupción del heap), sin excepción Delphi alguna
Los dos últimos puntos son la razón de que el bug costara tanto clavar. La validación paralela terminaba, el estado global corrompido se quedaba atrás, y el siguiente fixture del mismo proceso 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 sencillamente el primer código en usar PDFium tras el daño. Los números medidos dejan la escala a la vista. 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 lanzó directamente External exception C000001D. 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. Tras el arreglo, la misma sonda falló 0 de 1.200 documentos
Cómo se mantiene seguro ValidatePdfFilesParallel desde v3.125.1
ValidatePdfFilesParallel serializa ahora la mitad nativa de cada trabajo y mantiene paralela la mitad gestionada. Cada worker toma una sección crítica a nivel de unidad antes de crear su TPdf, y la retiene a través de FileName, Active := True, la construcción del informe de preflight, y Free. La creación y la destrucción están dentro del lock a propósito: cerrar un documento devuelve el control al módulo igual que la carga. Una vez el worker tiene capturado un registro TPdfPreflightReport, suelta el lock y evalúa las reglas de validación contra ese registro, que 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 el arreglo. Un fallo de carga ahora lanza EPdfError con LastLoadReport.ErrorMessage, así que el ErrorMessage del elemento nombra el problema de parseo real en lugar de un error secundario de «no hay documento activo». Y el coste se declara con honestidad: la parte PDFium del lote es ahora 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, fije WorkerCount a 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 = número de procesadores, con tope en 8
Options.Standards := [ppsPdfA];
// Con un registro explícito, seleccione usted el perfil correspondiente.
// Una lista Profiles vacía corre toda regla registrada, y las reglas de
// estándares sin preflight reportan «no pasaron»
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 registro es el camino corto: ValidatePdfFilesParallel crea entonces el registro por defecto ella misma, deduce la lista de perfiles de Options.Standards, y libera el registro al volver. Los resultados siempre regresan en orden de entrada, sea cual sea el orden en que acabaron los workers. Para los formatos de informe y el wrapper de línea de comandos sobre el mismo motor, vea informes de preflight PDF por lotes con la CLI de PDFium Component, y para qué cubren las comprobaciones PDF/A en sí, validación de preflight PDF/A en Delphi
¿Cómo ejecuta 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 llamador. Cada worker copia entonces la DLL PDFium cargada a un archivo con nombre único en el directorio temporal, carga esa copia con LoadLibrary, y la inicializa. Windows trata una DLL cargada desde otra ruta como otro módulo, así que cada copia obtiene 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 comprobaciones de cancelación entre pasos, y después destruye la biblioteca, 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 como mucho 4 workers, MaxPixelsPerPage y MaxTotalOutputBytes acotan la salida en crudo, y las opciones de render invertido y duotono nocturno se rechazan porque los buffers se devuelven en crudo. 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 basados en 1
Options := TPdfParallelRenderOptions.Default;
Options.Dpi := 150;
Options.MaxWorkers := 4;
// La instantánea de origen se toma sobre el módulo compartido, así que retenga el
// lock PDFium de todo el 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 lock alrededor de la llamada. Los módulos worker son privados, pero el paso de instantánea del principio corre SaveAs sobre el módulo compartido desde el hilo llamador. Si nada más en su proceso toca TPdf concurrentemente puede prescindir del lock; si algo lo hace, la instantánea necesita la misma protección que cualquier otra llamada al módulo compartido
| Patrón | Seguro entre documentos | El trabajo PDFium corre en paralelo | Coste |
|---|---|---|---|
Un TPdf por hilo, sin lock compartido | No | Sí, hasta que corrompe | Crasheos intermitentes, estado de proceso dañado |
| Un lock de todo el 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 | Parseo y preflight son serial |
TPdf.RenderPagesParallel | Sí | Sí | Copia de DLL, memoria y un parseo fresco por worker |
¿Cómo debe estructurar usted su propio código PDFium multihilo?
Sus propios hilos deberían compartir un único lock de todo el proceso y retenerlo durante toda la vida de cada TPdf que usen, o bien usar una API del componente que aísle el módulo por usted. El lock tiene que ser un único objeto para todo el proceso, no uno por hilo, por formulario o por documento; un lock 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 lock, y hacer todo lo que no toca PDFium fuera de él
uses
System.Classes, System.SysUtils, System.SyncObjs, PDFium;
var
PdfiumLock: TCriticalSection; // un lock 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 por debajo de 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 pocas reglas mantienen el patrón honesto en una aplicación real:
- Ponga
TPdf.CreateyFreedentro del lock, no solo las llamadas obvias. La carga, el cierre, las lecturas de propiedades comoPageCount, los cambios de página, la extracción de texto, el renderizado y el guardado alcanzan todos al módulo - Compruebe
Activetras asignarlo. Una carga fallida dejaActiveenFalse, yLastLoadReport.ErrorMessagedice por qué - Retenga el lock por documento en lugar 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 de la que el propio componente se fía - Mantenga el trabajo lento no PDFium, como escrituras a base de datos, indexación y llamadas de red, fuera del lock, o un solo consumidor lento serializará todo
- No trate el lock de render privado por instancia como un sustituto. Guarda un
TPdfcontra sí mismo y nada más
La misma cautela aplica al código que usted no escribió como hilos en crudo. Los futures en segundo plano son una buena manera de mantener los renders largos fuera del hilo de UI, como se describe en renderizado PDF en segundo plano con futures cancelables, pero el ejecutor de futures no añade un lock global de PDFium propio. Si varios futures pueden manejar instancias TPdf distintas a la vez, tome el mismo lock de todo el 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 API 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 de verdad para otra cosa que no sea renderizar páginas, los procesos worker separados le dan a cada trabajo su propio módulo por construcción
Referencia rápida: reglas de hilos PDFium para Delphi
- El estado inseguro de PDFium es de todo el módulo: la caché de fuentes, el page module y otros globales los comparten todos los documentos del proceso
- Un
TPdfpor hilo no aísla nada; dos instancias en dos hilos pueden seguir corrompiéndose - 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, tasks y futures necesitan un lock de todo el proceso que cubra cada
TPdfdeCreateaFree
PDFium Component envuelve el motor 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