PDFium ليست آمنة الخيوط على مستوى الوحدة، فمثيلتا TPdf تعملان على ملفين مختلفين في خيطين قد تفسد إحداهما الأخرى مع ذلك. يتولى PDFium Component for Delphi ذلك بطريقتين: منذ v3.125.1 يجمع ValidatePdfFilesParallel كل استدعاء PDFium أصلي خلف قفلٍ واحد على مستوى العملية، بينما تعطي TPdf.RenderPagesParallel كل عاملٍ نسخة معزولة خاصة به من وحدة PDFium. وكان العيب الذي فرض الإصلاح أسوأ أنواع الانقطاع: نجح اختبار تحقق دفعي أغلب الوقت، ثم بلّغ أحد ملفين صحيحين فاشلاً، ثم أسقط الاختبار التالي في العملية نفسها انتهاك وصول، وأحياناً أسقط العدّاء كله برمز خروج بدل تتبع مكدس. لم يكن خطأ في الاختبار، ولا خطأ في أي مستند بعينه. الافتراض كان الخطأ: TPdf واحد لكل خيط ليس عزلاً
لماذا لا يكفي TPdf واحد لكل خيط؟
TPdf واحد لكل خيط لا يكفي لأن PDFium تحفظ حالتها غير الآمنة في الوحدة لا في المستند. تملك كل TPdf معالج FPDF_DOCUMENT خاصاً بها، لكن كل معالج في العملية تخدمه الـ DLL المحملة نفسها، وتحمل تلك الـ DLL مفردات على مستوى العملية: ذاكرة الخطوط المؤقتة، ووحدة الصفحات، وبنى عامة أخرى تلمسها التحليل والتحميل والتصيير كلها. خيطان يحملان ملفين غير متصلين هما خيطان يكتبان في ذاكرة الخطوط المؤقتة نفسها في الوقت نفسه. لا أحد يملك تلك البيانات على جانب Delphi، فلا شيء على جانب Delphi يستطيع قفلها لكل مستند
للمكوّن قفلٌ فعلاً، ومن السهل استخلاص نتيجة خاطئة منه. يغلّف TPdf مسارات التصيير الخاصة بها في قسم حرج داخلي (EnterRenderLock / LeaveRenderLock، تابعتان خاصتان لـ TPdf). ذلك القفل لكل مثيل. يمنع خيطين من قيادة TPdf نفسها في وقت واحد، وهو خطر حقيقي، لكنه لا يرى مثيلاً ثانياً على خيط آخر، فيمر التزامن عبر المثيلات من جانبه مباشرة. والقاعدة العامة بسيطة تتسع في سطر: في وحدة PDFium محملة واحدة يُجاز لمؤخلٍ واحد على الأكثر أن يكون داخل PDFium في أي لحظة، أياً كان عدد المستندات المفتوحة
ماذا تبدو الإفساد عبر المستندات في عملية Delphi؟
يبدو الإفساد عبر المستندات مزيجاً عشوائياً من إخفاقات غير متصلة، ويعيش الضرر أطول من الكود الذي سببه. قبل v3.125.1 أنشأ ValidatePdfFilesParallel TPdf لكل خيط عامل وشغّل Active := True وبناء تقرير الفحص المسبق تزامناً على الوحدة المشتركة. والأعراض المرصودة على بناءات Delphi و Free Pascal غطت المدى كله:
- ملف صحيح يفشل تحميله، أو يعود من الدفعة فاشلاً حين كان يجب أن يجتاز
- انتهاك وصول يظهر في استدعاء لاحق غير متصل، غالباً في اختبار مختلف أو مستند مختلف
- يظهر
External exception C000001Dفي Delphi. ذلك الرمز هوSTATUS_ILLEGAL_INSTRUCTION، يرفعه تعليمةud2التي تنفذها ماكروCHECKوIMMEDIATE_CRASHالداخليان في PDFium حين تنكسر ثابتة - تخرج العملية بـ
0xC0000409(fail-fast، تُبلَّغ فيضان مخزن مكدس) أو0xC0000374(إفساد كومة)، من دون أي استثناء Delphi
النقطتان الأخيرتان هما سبب صعوبة تثبيت العيب. اكتمل التحقق المتوازي، وبقيت الحالة العامة المفسدة خلفه، وعثرت التجهيزة التالية في العملية نفسها عليها. في تشغيل انحدار Delphi Win64 صابت موجة من إخفاقات C000001D اختباراتٍ لم تلمس التحقق الدفعي قط؛ كانت ببساطة أول كود يستخدم PDFium بعد الضرر. والأرقام المقيسة تجعل الحجم جلياً: فحص Delphi شغّل العينة نفسها عبر عاملين فشل في 122 من 160 مستنداً في تشغيل واحد و 138 من 160 في آخر، ورفع أحد ذلك التشغيلين External exception C000001D صريحة. وحالة إجهاد من 8 مستندات و 4 عمال و 5 جولات فشلت أو انهارت في 5 من 5 تشغيلات على Free Pascal Win64. بعد الإصلاح فشل الفحص نفسه في 0 من 1,200 مستند
كيف يبقى ValidatePdfFilesParallel آمناً منذ v3.125.1
يجمع ValidatePdfFilesParallel الآن النصف الأصلي من كل مهمة ويبقي النصف المُدار متوازياً. يأخذ كل عامل قسماً حرجاً على مستوى الوحدة قبل أن ينشئ TPdf خاصته، ويحمله عبر FileName و Active := True وبناء تقرير الفحص المسبق و Free. الإنشاء والإتلاف داخل القفل عن قصد: إغلاق مستند يعود استدعاءً إلى الوحدة كما يفعل التحميل. وبمجرد امتلاك العامل سجل TPdfPreflightReport ملتقطاً يحرر القفل ويقيّم قواعد التحقق مقابل ذلك السجل، الذي لا يلمس حالة PDFium، فيتداخل تقييم القواعد لملفٍ مع عمل PDFium للملف التالي
جاء مع الإصلاح تغييران أصغر. يرفع إخفاقُ تحميل الآن EPdfError مع LastLoadReport.ErrorMessage، فيسمي ErrorMessage الخاص بالبند مشكلة التحليل الفعلية بدل خطأ ثانوي «لا مستند نشط». والكلفة مصرَّح بها بأمانة: صار جزء PDFium من الدفعة تسلسلياً، فعلى دفعة يغلب عليها التحليل والفحص المسبق لا تشتري العمالُ الإضافيون شيئاً يذكر. وإن كنت على إصدار قبل v3.125.1 اضبط WorkerCount على 1؛ ذلك يزيل التزامن والإفساد معه
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 = عدد المعالجات، مسقوف عند 8
Options.Standards := [ppsPdfA];
// مع سجل صريح انتقِ الملف الجانبي المطابق بنفسك.
// قائمة Profiles الفارغة تشغل كل قاعدة مسجلة، وقواعد
// المعايير التي لم تفحصها تبلّغ «لم تجتز»
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;
تمرير nil بوصفه سجلاً هو الطريق الأقصر: ينشئ ValidatePdfFilesParallel السجل الافتراضي بنفسه حينئذٍ، ويشتق قائمة الملفات الجانبية من Options.Standards، ويحرر السجل عند عودته. تعود النتائج دوماً بترتيب الإدخال أياً كان ترتيب انتهاء العمال. ولصيغ التقارير والغلاف سطر الأوامر حول المحرك نفسه راجع تقارير الفحص المسبق الدفعية لملفات PDF مع واجهة PDFium Component الطرفية، ولما تغطيه فحوص PDF/A ذاتها التحقق المسبق من PDF/A في Delphi
كيف تشغّل RenderPagesParallel الصفحات بالتوازي فعلاً؟
تعمل TPdf.RenderPagesParallel بالتوازي لأن عمالها لا يتشاركون وحدة PDFium قط. تحفظ الطريقة أولاً المستند النشط في مخزن مصدر على الخيط المستدعي. ثم تنسخ كل عامل الـ DLL لـ PDFium المحملة إلى ملف مسمى فريداً في مجلد temp، وتحمل تلك النسخة بـ LoadLibrary، وتهيئها. يعامل Windows الـ DLL المحملة من مسار مختلف وحدةً مختلفة، فتكسب كل نسخة مفرداتها الخاصة: ذاكرة خطوطها المؤقتة الخاصة، ووحدة صفحاتها الخاصة، وكل شيء خاصاً بها. تفتح العاملة المستند المحفوظ في وحدتها الخاصة، وتصيّر صفحاتها تدريجياً بفحوص إلغاء بين الخطوات، ثم تدمر المكتبة، وتفرغ النسخة، وتحذف الملف
العزل ليس مجانياً، والافتراضات تعكس ذلك. تدفع كل عاملة كلفة نسخة DLL على القرص، ومجموعة ثانية من مفردات PDFium في الذاكرة، وتحليلاً جديداً للمستند. MaxWorkers = 0 يعني 4 عمال على الأكثر، ويسقف MaxPixelsPerPage و MaxTotalOutputBytes الناتجَ الخام، ويُرفض خيارا التصيير المعكوس وثنائي اللون الليلي لأن المخازن تعود خاماً. والناتج TPdfParallelRenderReport تحمل مصفوفة Results فيه مخزناً واحداً 32-بت من الأعلى للأسفل لكل صفحة مطلوبة، بترتيب الطلب
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; // أرقام الصفحات تبدأ من 1
Options := TPdfParallelRenderOptions.Default;
Options.Dpi := 150;
Options.MaxWorkers := 4;
// تؤخذ لقطة المصدر على الوحدة المشتركة، فامسك
// قفل PDFium على مستوى العملية إن استخدمت خيوط أخرى 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;
لاحظ القفل حول الاستدعاء. وحدات العمال خاصة، لكن خطوة اللقطة في البداية تشغّل SaveAs على الوحدة المشتركة من الخيط المستدعي. فإن لم يلمس أي شيء آخر في عمليتك TPdf تزامناً أسقطت القفل؛ وإن لمس شيء ذلك احتاجت اللقطة الحماية نفسها التي يحتاجها كل استدعاء آخر للوحدة المشتركة
| النمط | آمن عبر المستندات | عمل PDFium يجري بالتوازي | الكلفة |
|---|---|---|---|
TPdf واحد لكل خيط، بلا قفل مشترك | لا | نعم، حتى تفسد | انهيارات متقطعة، حالة عملية تالفة |
| قفل واحد على مستوى العملية حول كل استدعاءات PDFium | نعم | لا | جزء PDFium تسلسلي |
ValidatePdfFilesParallel منذ v3.125.1 | نعم | لا؛ تقييم القواعد متوازٍ | التحليل والفحص المسبق تسلسليان |
TPdf.RenderPagesParallel | نعم | نعم | نسخة DLL وذاكرة وتحليل جديد لكل عامل |
كيف تبني كود PDFium متعدد الخيوط خاصتك؟
ينبغي لخيوطك أن تتشارك قفلاً واحداً على مستوى العملية وتمدده على الحياة الكاملة لكل TPdf تستخدمه، أو تستخدم واجهة مكوّن تعزلك الوحدة عنك. القفل لا بد أن يكون كائناً واحداً للعملية كلها، لا واحداً لكل خيط أو نموذج أو مستند؛ قفل لا يتشاركه خيطان لا يحمي شيئاً. يحاكي النمط أدناه ما يفعله المكوّن داخلياً منذ v3.125.1: إنشاء وتحميل وقراءة وتحرير داخل القفل، ثم كل ما لا يلمس PDFium خارجه
uses
System.Classes, System.SysUtils, System.SyncObjs, PDFium;
var
PdfiumLock: TCriticalSection; // قفل واحد للعملية كلها
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; // إغلاق المستند عمل PDFium أيضاً
end;
finally
PdfiumLock.Release;
end;
// لا PDFium تحت هذا السطر، فهذا الجزء يعمل بالتوازي
FText := Raw.ToString.Trim;
finally
Raw.Free;
end;
end;
initialization
PdfiumLock := TCriticalSection.Create;
finalization
PdfiumLock.Free;
بضعة قواعد تبقي النمط صادقاً في تطبيق حقيقي:
- ضع
TPdf.CreateوFreeداخل القفل، لا الاستدعاءات الواضحة وحدها. التحميل والإغلاق وقراءات الخصائص مثلPageCountوتغييرات الصفحات واستخلاص النص والتصيير والحفظ كلها تمد يدها إلى الوحدة - افحص
Activeبعد إسناده. يبقي التحميل الفاشلActiveعلىFalse، ويقولLastLoadReport.ErrorMessageالسبب - امسك القفل لكل مستند لا لكل استدعاء. القفل الأدق ممكن مبدئياً، لكن فقط إذا لم يجري أي عضو من أعضاء
TPdfخارجه قط، والنسخة الخشنة هي التي يعتمد عليها المكوّن نفسه - أبقِ العمل البطيء غير PDFium، ككتابات قواعد البيانات والفهرسة واستدعاءات الشبكة، خارج القفل، وإلا جمع مستهلك بطيء واحد كل شيء
- لا تعامل قفل التصيير الداخلي لكل مثيل بديلاً. يحرس
TPdfواحداً ضد نفسه ولا أكثر
يسري الحذر نفسه على كود لم تكتبه خيوطاً خاماً. العقود المستقبلية الخلفية طريقة جيدة لإبقاء التصييرات الطويلة بعيداً عن خيط الواجهة، كما في تصيير PDF في الخلفية بعقود مستقبلية قابلة للإلغاء، لكن منفذ العقد لا يضيف قفل PDFium عاماً من عنده. فإن استطاعت عدة عقود قيادة مثيلات TPdf مختلفة في الوقت نفسه، خذ القفل نفسه على مستوى العملية داخل كل عامل، وعامل العارض على الخيط الرئيس عميلاً إضافياً للوحدة المشتركة. الاستخدام عبر المثيلات عبر الواجهات غير المتزامنة لم يُدقق على حدة، فالافتراض المتحفظ أنه يحتاج التجميع نفسه الذي تحتاجه الخيوط المكتوبة يدوياً. وحين تحتاج توازي PDFium حقيقياً لشيء سوى تصيير الصفحات، تعطي عمليات العامل المنفصلة كل مهمة وحدتها الخاصة بالبناء
مرجع سريع: قواعد خيوط PDFium لـ Delphi
- حالة PDFium غير الآمنة على مستوى الوحدة كلها: ذاكرة الخطوط المؤقتة ووحدة الصفحات والمفردات الأخرى تتشاركها كل مستندات العملية
-
TPdfواحد لكل خيط لا يعزل شيئاً؛ مثيلان على خيطين قد يفسد إحداهما الأخرى مع ذلك - الأعراض النموذجية إخفاقات تحميل وانتهاكات وصول في كود لاحق و
External exception C000001Dوخروج بـ0xC0000409أو0xC0000374 - الإفساد يبقى في العملية، فالاستدعاء الفاشل غالباً ليس الذي سببه
-
ValidatePdfFilesParallelآمنة منذ v3.125.1؛ وعلى الإصدارات الأقدم استخدمWorkerCount := 1 -
TPdf.RenderPagesParallelمتوازية فعلاً لأن كل عامل تحمّل نسخة معزولة من وحدة PDFium - تحتاج خيوطك ومهامك وعقودك قفلاً واحداً على مستوى العملية يغطي كل
TPdfمنCreateإلىFree
يغلّف PDFium Component محرك PDFium لـ Delphi بفحص مسبق وتحقق دفعي وتصيير متوازٍ معزول وعمل خلفي قابل للإلغاء وتشخيصات تحميل مفصلة. التفاصيل والإصدارات في صفحة منتج PDFium Component