فئة الموضوع في دلفي
رابتور [الاستوديو العقلي]
http://mental.mentu.com
أربعة
القسم الحرج (CriticalSection) هو تقنية لحماية الوصول إلى البيانات المشتركة. إنه في الواقع يعادل متغير منطقي عالمي. لكن عملها مختلف، فهي تحتوي على عمليتين فقط: الدخول والخروج، ويمكن أيضًا اعتبار حالتيها صحيحة وخاطئة، مما يشير إلى ما إذا كانت في القسم الحرج. تعتبر هاتان العمليتان أيضًا عمليات أولية، لذا يمكن استخدامها لحماية البيانات المشتركة من انتهاكات الوصول في التطبيقات متعددة الخيوط.
طريقة استخدام الأقسام الهامة لحماية البيانات المشتركة بسيطة للغاية: قم باستدعاء Enter لتعيين علامة القسم الحرج قبل كل وصول إلى البيانات المشتركة، ثم قم بتشغيل البيانات، وأخيرًا اتصل بـ Leave لمغادرة القسم الحرج. مبدأ الحماية الخاص به هو كما يلي: بعد دخول مؤشر الترابط إلى القسم الحرج، إذا أراد مؤشر ترابط آخر أيضًا الوصول إلى البيانات في هذا الوقت، فستجد أن مؤشر الترابط قد دخل بالفعل إلى القسم الحرج عند استدعاء Enter، ثم سيكون هذا الموضوع قم بتعليق المكالمة وانتظر حتى يتصل مؤشر الترابط الموجود حاليًا في القسم الحرج بالمغادرة لمغادرة القسم الحرج. عندما يكمل مؤشر ترابط آخر العملية ويستدعي المغادرة للمغادرة، سيتم تنشيط هذا الخيط وتعيين علامة القسم الحرج وبدء تشغيل البيانات. وبالتالي منع الصراع الوصول.
لنأخذ InterlockedIncrement السابق كمثال، نستخدم CriticalSection (Windows API) لتنفيذه:
فار
InterlockedCrit : TRTLCriticalSection;
PROcedure InterlockedIncrement( var aValue : Integer );
يبدأ
EnterCriticalSection(InterlockedCrit);
Inc(aValue);
LeaveCriticalSection(InterlockedCrit);
نهاية؛
والآن أنظر إلى المثال السابق:
1. يدخل الخيط A إلى القسم الحرج (بافتراض أن البيانات هي 3)
2. يدخل مؤشر الترابط B إلى القسم الحرج لأن A موجود بالفعل في القسم الحرج، ويتم تعليق B.
3. يضيف الموضوع A واحدًا إلى البيانات (الآن 4)
4. يترك الخيط A القسم الحرج وينشط الخيط B (البيانات الحالية في الذاكرة هي 4)
5. ينشط مؤشر الترابط B ويضيف واحدًا إلى البيانات (الآن هو 5)
6. يترك الخيط B القسم الحرج، والبيانات الحالية صحيحة.
هذه هي الطريقة التي تحمي بها الأقسام المهمة الوصول إلى البيانات المشتركة.
فيما يتعلق باستخدام الأقسام الهامة، هناك شيء واحد يجب ملاحظته وهو التعامل مع الاستثناءات أثناء الوصول إلى البيانات. لأنه في حالة حدوث استثناء أثناء تشغيل البيانات، لن يتم تنفيذ عملية الإجازة، ونتيجة لذلك، لن يتم تنشيط الخيط الذي يجب تنشيطه، مما قد يتسبب في عدم استجابة البرنامج. لذا بشكل عام، فإن النهج الصحيح هو استخدام الأقسام الهامة على النحو التالي:
أدخل القسم الحرج
يحاول
// تشغيل بيانات القسم الهامة
أخيراً
اترك القسم الحرج
نهاية؛
آخر شيء يجب ملاحظته هو أن Event وCriticalSection كلاهما من موارد نظام التشغيل، ويجب إنشاؤهما قبل الاستخدام وإصدارهما بعد الاستخدام. على سبيل المثال، الحدث العام: SyncEvent والقسم العام CriticalSection: TheadLock المستخدم بواسطة فئة TThread يتم إنشاؤهما وإصدارهما في InitThreadSynchronization وDoneThreadSynchronization، ويتم استدعاؤهما في وحدة التهيئة والإنهاء للفئات.
نظرًا لاستخدام واجهات برمجة التطبيقات (APIs) لتشغيل Event وCriticalSection في TThread، فقد تم استخدام API كمثال أعلاه، وفي الواقع، قدمت دلفي تغليفًا لهما في وحدة SyncObjs، وهما فئة TEvent وفئة TCriticalSection على التوالي. الاستخدام هو تقريبًا نفس الطريقة السابقة لاستخدام API. نظرًا لأن منشئ TEvent يحتوي على عدد كبير جدًا من المعلمات، فمن أجل البساطة، توفر دلفي أيضًا فئة حدث تمت تهيئتها باستخدام المعلمات الافتراضية: TSimpleEvent.
بالمناسبة، اسمحوا لي أن أقدم فئة أخرى تستخدم لمزامنة مؤشر الترابط: TMultiReadExclusiveWriteSynchronizer، والتي تم تعريفها في وحدة SysUtils. بقدر ما أعرف، هذا هو أطول اسم فئة تم تعريفه في Delphi RTL، ولحسن الحظ، فإنه يحتوي على اسم مستعار قصير: TMREWSync. أما بالنسبة لاستخدامه، أعتقد أنه يمكنك معرفته بمجرد النظر إلى الاسم، لذلك لن أقول المزيد.
من خلال المعرفة التحضيرية السابقة حول الحدث والقسم الحرج، يمكننا البدء رسميًا في مناقشة المزامنة والانتظار.
نحن نعلم أن المزامنة تحقق مزامنة الخيط عن طريق وضع جزء من التعليمات البرمجية في الخيط الرئيسي للتنفيذ، لأنه في العملية يوجد خيط رئيسي واحد فقط. دعونا نلقي نظرة أولاً على تنفيذ المزامنة:
الإجراء TThread.Synchronize(Method: TThreadMethod);
يبدأ
FSynchronize.FThread := Self;
FSynchronize.FSynchronizeException := nil;
FSynchronize.FMethod := الطريقة؛
مزامنة(@FSynchronize);
نهاية؛
حيث FSynchronize هو نوع سجل:
PSynchronizeRecord = ^TSynchronizeRecord;
TSynchronizeRecord = السجل
FThread: TObject;
FMethod: TThreadMethod؛
FSynchronizeException: TObject;
نهاية؛
يستخدم لتبادل البيانات بين الخيوط والخيط الرئيسي، بما في ذلك كائنات فئة الخيوط الواردة وطرق المزامنة والاستثناءات التي تحدث.
يُطلق على النسخة المحملة بشكل زائد اسم "المزامنة"، وهذه النسخة المحملة بشكل زائد خاصة جدًا، فهي "طريقة فئة". ما يسمى بطريقة الفئة هي طريقة عضو فئة خاصة، ولا يتطلب استدعاءها إنشاء مثيل فئة، ولكن يتم استدعاؤها من خلال اسم الفئة مثل المُنشئ. السبب وراء تنفيذه باستخدام طريقة الفصل هو أنه يمكن استدعاؤه حتى عندما لا يتم إنشاء كائن مؤشر الترابط. ومع ذلك، في الممارسة العملية، يتم استخدام إصدار آخر مثقل (أيضًا طريقة فئة) وطريقة فئة أخرى StaticSynchronize. إليك رمز هذه المزامنة:
إجراء الفصل TThread.Synchronize(ASyncRec: PSynchronizeRecord);
فار
سينكبروك: تسينكبروك؛
يبدأ
إذا كان GetCurrentThreadID = MainThreadID إذن
ASyncRec.FMethod
آخر
يبدأ
SyncProc.Signal := CreateEvent(nil, True, False, nil);
يحاول
EnterCriticalSection(ThreadLock);
يحاول
إذا SyncList = لا شيء ثم
SyncList := TList.Create;
SyncProc.SyncRec := ASyncRec;
SyncList.Add(@SyncProc);
this.SignalSyncEvent;
إذا تم تعيينه (WakeMainThread) بعد ذلك
WakeMainThread(SyncProc.SyncRec.FThread);
LeaveCriticalSection(ThreadLock);
يحاول
WaitForSingleObject(SyncProc.Signal, INFINITE);
أخيراً
EnterCriticalSection(ThreadLock);
نهاية؛
أخيراً
LeaveCriticalSection(ThreadLock);
نهاية؛
أخيراً
CloseHandle(SyncProc.Signal);
نهاية؛
إذا تم تعيينه (ASyncRec.FSynchronizeException) ثم قم برفع ASyncRec.FSynchronizeException؛
نهاية؛
نهاية؛
هذا الرمز أطول قليلاً، لكنه ليس معقدًا للغاية.
الأول هو تحديد ما إذا كان الخيط الحالي هو الخيط الرئيسي. إذا كان الأمر كذلك، فما عليك سوى تنفيذ طريقة المزامنة والعودة.
إذا لم يكن الموضوع الرئيسي، فهو جاهز لبدء عملية المزامنة.
يتم تسجيل بيانات تبادل الخيوط (المعلمات) ومقبض الحدث من خلال المتغير المحلي SyncProc. وتكون بنية السجل كما يلي:
TSyncProc=سجل
SyncRec: PSynchronizeRecord؛
الإشارة: المقبض؛
نهاية؛
ثم قم بإنشاء حدث، ثم أدخل القسم الحرج (من خلال المتغير العام ThreadLock، لأن مؤشر ترابط واحد فقط يمكنه الدخول إلى حالة المزامنة في نفس الوقت، حتى تتمكن من استخدام المتغير العام للتسجيل)، ثم قم بتخزين البيانات المسجلة في قائمة SyncList (إذا كانت هذه القائمة غير موجودة، فقم بإنشائها). يمكن ملاحظة أن القسم المهم في ThreadLock هو حماية الوصول إلى SyncList، وسيظهر هذا مرة أخرى عند تقديم CheckSynchronize لاحقًا.
الخطوة التالية هي استدعاء SignalSyncEvent. تم تقديم الكود الخاص به من قبل عند تقديم مُنشئ TThread. وتتمثل وظيفته في إجراء عملية Set على SyncEvent. سيتم تفصيل الغرض من SyncEvent هذا لاحقًا عند تقديم WaitFor.
التالي هو الجزء الأكثر أهمية: استدعاء حدث WakeMainThread لعمليات المزامنة. يعد WakeMainThread حدثًا عالميًا من النوع TNotifyEvent. سبب استخدام الأحداث للمعالجة هنا هو أن طريقة المزامنة تضع بشكل أساسي العملية التي تحتاج إلى المزامنة في مؤشر الترابط الرئيسي للتنفيذ من خلال الرسائل. ولا يمكن استخدامها في بعض التطبيقات التي لا تحتوي على حلقات رسائل (مثل وحدة التحكم أو DLL). لذا استخدم هذا الحدث للمعالجة.
يستجيب كائن التطبيق لهذا الحدث، ويتم استخدام الطريقتين التاليتين لتعيين ومسح الاستجابة لحدث WakeMainThread (من وحدة النماذج):
الإجراء TApplication.HookSynchronizeWakeup؛
يبدأ
Classes.WakeMainThread := WakeMainThread;
نهاية؛
الإجراء TApplication.UnhookSynchronizeWakeup؛
يبدأ
Classes.WakeMainThread := nil;
نهاية؛
يتم استدعاء الطريقتين المذكورتين أعلاه في المُنشئ والمدمر لفئة TApplication على التوالي.
هذا هو الرمز الذي يستجيب لحدث WakeMainThread في كائن التطبيق، ويتم إرسال الرسالة هنا ويستخدم رسالة فارغة لتحقيق ذلك:
الإجراء TApplication.WakeMainThread(Sender: TObject);
يبدأ
PostMessage(Handle, WM_NULL, 0, 0);
نهاية؛
الرد على هذه الرسالة موجود أيضًا في كائن التطبيق، راجع الكود التالي (قم بإزالة الأجزاء غير ذات الصلة):
الإجراء TApplication.WndProc(var الرسالة: TMessage);
…
يبدأ
يحاول
…
مع رسالة القيام به
حالة الرسالة
…
WM_NULL:
CheckSynchronize;
…
يستثني
HandleException(Self);
نهاية؛
نهاية؛
من بينها، يتم تعريف CheckSynchronize أيضًا في وحدة Classes نظرًا لأنها معقدة نسبيًا، فلن نشرحها بالتفصيل في الوقت الحالي. فقط اعلم أن هذا هو الجزء الذي يتعامل مع وظيفة المزامنة على وجه التحديد شفرة.
بعد تنفيذ حدث WakeMainThread، اخرج من القسم الحرج، ثم اتصل بـ WaitForSingleObject لبدء انتظار الحدث الذي تم إنشاؤه قبل الدخول إلى القسم الحرج. وظيفة هذا الحدث هي الانتظار حتى انتهاء تنفيذ طريقة المزامنة هذه، وسيتم شرح ذلك لاحقًا عند تحليل CheckSynchronize.
لاحظ أنه بعد WaitForSingleObject، يمكنك إعادة الدخول إلى القسم الحرج، ولكن الخروج دون القيام بأي شيء يبدو بلا معنى، ولكنه ضروري!
لأن الدخول والخروج في القسم الحرج يجب أن يتوافق تمامًا مع واحد لواحد. فهل يمكن تغييره إلى هذا:
إذا تم تعيينه (WakeMainThread) بعد ذلك
WakeMainThread(SyncProc.SyncRec.FThread);
WaitForSingleObject(SyncProc.Signal, INFINITE);
أخيراً
LeaveCriticalSection(ThreadLock);
نهاية؛
أكبر فرق بين الكود أعلاه والكود الأصلي هو أن WaitForSingleObject متضمن أيضًا في قيود القسم الحرج. يبدو أنه ليس له أي تأثير، وهو يبسط الكود إلى حد كبير، ولكن هل هذا ممكن حقًا؟
في الواقع، لا!
لأننا نعلم أنه بعد قسم الإدخال الحرج، إذا أرادت سلاسل الرسائل الأخرى الدخول مرة أخرى، فسيتم تعليقها. سوف يقوم أسلوب WaitFor بتعليق مؤشر الترابط الحالي ولن يتم تنشيطه حتى ينتظر SetEvent لمؤشرات الترابط الأخرى. إذا تم تغيير الرمز إلى ما سبق، وإذا كان مؤشر ترابط SetEvent يحتاج أيضًا إلى الدخول إلى القسم الحرج، فسيحدث طريق مسدود (للاطلاع على نظرية الجمود، يرجى الرجوع إلى المعلومات المتعلقة بمبادئ نظام التشغيل).
يعد الجمود أحد أهم جوانب مزامنة الخيط!
أخيرًا، سيتم إصدار الحدث الذي تم إنشاؤه في البداية إذا أعادت الطريقة المتزامنة استثناءً، فسيتم طرح الاستثناء مرة أخرى هنا.
(يتبع)