التزامن الآمن في Ada ── دليل عملي للمهامّ (Tasks) والكائنات المحمية (Protected Objects)
· آخر تحديث: · غو كومورا · Ada, التزامن, المهامّ, الكائنات المحمية, الرندفو, الزمن الحقيقي, البرمجة المتوازية, لغة برمجة, المعالجة المتزامنة, الموثوقية العالية
1. مقدّمة ── التزامن المدمج في اللغة نفسها
التزامن (concurrency) موضوع لا يمكن تجاوزه في تطوير البرمجيات الحديث. لكن في كثير من اللغات، يعتمد التزامن على مكتبات “مُلحَقة لاحقاً” أو على ميزات نظام التشغيل، ما يتطلّب معرفة عميقة وتصميماً حذراً لاستخدامه بشكل صحيح.
تملك Ada إجابتها الخاصّة عن هذه المشكلة. التزامن مدمج في مواصفة اللغة نفسها.
نموذج التزامن في Ada:
- المهمّة (task) ── وحدة تزامن مستقلّة التنفيذ
- الرندفو (rendezvous) ── تواصل متزامن بين المهامّ
- الكائن المحمي (protected object) ── تحكّم حصريّ تديره اللغة
- أولويّة الزمن الحقيقيّ ── ميزات Annex D للزمن الحقيقيّ
المهامّ والرندفو موجودان منذ Ada 83 عام 1983، وأُضيفت الكائنات المحميّة وميزات الزمن الحقيقيّ في Annex D في Ada 95، ثمّ تطوّرت اللغة عبر Ada 2005 و2012. أبرز ما يميّز التزامن في Ada هو أنّه يتيح التعبير عن نيّة التصميم مباشرةً في الشيفرة، بدلاً من الاعتماد على أدوات تزامن منخفضة المستوى مثل الميوتكس (mutex) أو السيمافور (semaphore).
في هذا المقال، نشرح التزامن في Ada تدريجيّاً عبر ثمانية أمثلة شيفرة عمليّة. كلّ مثال قابل للترجمة (compile) والتنفيذ فعليّاً كمقتطف مستقلّ (snippet)، ويمكنك تجربته بنفسك.
كما أنّ مقتطفات الشيفرة الواردة في هذا المقال منشورة على GitHub كمجموعة شيفرة مرجعيّة منظّمة في ملفّات حسب كلّ فصل.
ada-task-concurrency - komurasoft-blog-samples (GitHub)
2. مراجعة “مخاطر” التزامن
قبل الدخول في تفاصيل Ada، دعونا نتحقّق بإيجاز من سبب أهمية التزامن “الآمن”.
من أشهر الأخطاء البرمجية (bugs) الشائعة في التزامن ما يلي.
- تسابق البيانات (data race): عندما تصل عدّة خيوط إلى موضع الذاكرة نفسه في الوقت ذاته، ويكون أحدها على الأقلّ عمليّة كتابة. النتيجة غير مُعرَّفة (undefined).
- التوقّف التام (deadlock): حالة تظلّ فيها عدّة مهامّ تنتظر اكتمال بعضها بعضاً إلى الأبد دون أن يتقدّم أيّ منها.
- انعكاس الأولويّة (priority inversion): مشكلة تنتظر فيها مهمّة عالية الأولويّة موارد تحتفظ بها مهمّة منخفضة الأولويّة، بينما تسبق مهمّة متوسّطة الأولويّة المهمّة منخفضة الأولويّة (preempt).
- التجويع (starvation): حالة لا تستطيع فيها مهمّة ما الحصول على مورد إلى الأبد.
يوفّر نموذج التزامن في Ada إجراءات دفاعية على مستوى اللغة نفسها ضدّ هذه المشكلات.
تسابق البيانات ← الكائن المحمي يضمن الوصول الحصري
التوقف التام ← نموذج الرندفو يوفر تزامناً بنيوياً
انعكاس الأولوية ← بروتوكول سقف الأولوية (Priority Ceiling Protocol) متاح مدمجاً في اللغة
التجويع ← يُتحكَّم به عبر حواجز المداخل وسياسات الطابور
3. أساسيّات المهمّة (task) ── وحدة تنفيذ مستقلّة
الوحدة الأساسيّة للتزامن في Ada هي المهمّة (task). تشبه المهمّة الخيط (thread)، لكنّها لا تقابل بالضرورة خيط نظام التشغيل واحداً بواحد، إذ تتولّى بيئة تشغيل Ada إدارة الجدولة.
task Greeter is
entry Start;
end Greeter;
task body Greeter is
begin
accept Start;
Put_Line ("Hello from a task!");
end Greeter;
في هذه الشيفرة (01_hello_task.ada) عدّة نقاط مهمّة.
تبدأ المهمّة تنفيذها تلقائيّاً بمجرّد الإعلان عنها. تُشغَّل مهمّة Greeter عند الوصول إلى begin في الإجراء الذي يحتويها، وتنتظر عند accept Start; طلب الرندفو القادم من المستدعي.
المدخل (entry) هو الواجهة التي تعرضها المهمّة إلى الخارج. عندما يستدعي المستدعي Greeter.Start;، يحدث تزامن مع accept Start; في المهمّة. يُسمّى هذا الرندفو.
يُنتظَر انتهاء المهمّة تلقائيّاً. عندما ينتهي الإجراء الرئيسي، إذا كانت هناك مهامّ لا تزال قيد التنفيذ، يُنتظَر اكتمالها ضمنيّاً. وهذا يتناقض مع الانهيار (crash) الناتج عن نسيان استدعاء std::thread::join في C++.
4. الرندفو ── تواصل متزامن ينقل البيانات
لا يقتصر الرندفو على التزامن فحسب، بل يمكنه أيضاً تمرير البيانات في الاتّجاهين.
task Worker is
entry Compute (X, Y : Integer; Result : out Integer);
end Worker;
task body Worker is
A, B : Integer;
Output : Integer;
begin
accept Compute (X, Y : Integer; Result : out Integer) do
A := X;
B := Y;
Output := A * A + B * B;
Result := Output;
end Compute;
end Worker;
يستخدمه المستدعي على النحو التالي (02_rendezvous_intro.ada).
Worker.Compute (3, 4, Answer);
Put_Line ("Main: result = " & Integer'Image (Answer));
النقطة التصميميّة المهمّة هنا هي أنّ نمط المعامل مُصرَّح به بوضوح.
- نمط
in: تمرير قيمة من المستدعي إلى المهمّة - نمط
out: إعادة النتيجة من المهمّة إلى المستدعي - نمط
in out: في الاتّجاهين
تُصبح الكتلة do ... end داخل جسم accept قسماً حرجاً (critical section). خلال هذه الفترة يُحجَب المستدعي، ولا تقبل المهمّة أيّ مدخل آخر. وعند اكتمال المعالجة يستأنف الطرفان.
خلاصة خصائص الرندفو:
| الخاصيّة | الوصف |
|---|---|
| التزامن | ينتظر المستدعي والمهمّة معاً حتّى يصلا إلى نقطة الرندفو في الوقت نفسه |
| نقل البيانات | يمكن تمرير القيم في الاتّجاهين عبر معاملات in / out / in out |
| التحكّم الحصريّ | تُحجَب بقيّة مداخل المهمّة أثناء تنفيذ جسم accept |
| البنية | يُصرَّح في جسم المهمّة بوضوح متى يُقبَل كلّ مدخل |
5. القبول الانتقائي (selective accept) ── انتظار عدّة خدمات
في مهامّ الخادم الفعليّة، يلزم انتظار عدّة أنواع من الطلبات. تحقّق جملة select في Ada هذا على مستوى اللغة نفسها.
task Server is
entry Deposit (Amount : Integer);
entry Withdraw (Amount : Integer; Success : out Boolean);
entry Balance (Value : out Integer);
end Server;
task body Server is
Current : Integer := 0;
begin
loop
select
accept Deposit (Amount : Integer) do
Current := Current + Amount;
end Deposit;
or
accept Withdraw (Amount : Integer; Success : out Boolean) do
if Current >= Amount then
Current := Current - Amount;
Success := True;
else
Success := False;
end if;
end Withdraw;
or
accept Balance (Value : out Integer) do
Value := Current;
end Balance;
or
terminate;
end select;
end loop;
end Server;
تحتوي جملة select في هذه الشيفرة (03_selective_accept.ada) على عدّة فروع or، ويُختار أحد المداخل التي وردت لها استدعاءات (الاختيار مُعرَّف حسب المترجم/implementation-defined). وإذا لم يُستدعَ أيّ مدخل، تنتظر المهمّة حتّى يُستدعى أحدها.
or terminate; فرع خاصّ يُنهي المهمّة بأمان عندما “ينتهي الإجراء الرئيسي ولم يعد هناك احتمال أن يستدعي أحد مدخلاً لهذه المهمّة”. إنّها آليّة خاصّة بـ Ada تحلّ مشكلة “مهمّة الخادم التي تظلّ منتظِرة إلى الأبد” التي تسبّب التوقّف التام.
ما يجعل القبول الانتقائي قويّاً هو إمكان كتابة شروط الحرس (guard conditions) أيضاً.
select
when Count > 0 =>
accept Get_Item (Item : out Integer) do
Item := Data (Head);
Count := Count - 1;
end Get_Item;
or
when Count < Max =>
accept Put_Item (Item : Integer) do
Data (Tail) := Item;
Count := Count + 1;
end Put_Item;
end select;
يُستبعَد الفرع الذي يكون شرط الحرس فيه خاطئاً من الاختيار في تلك اللحظة. وبذلك يمكن كتابة تحكّم مثل “إن كان المخزن فارغاً يُنتظَر Get، وإن كان ممتلئاً يُنتظَر Put” بأسلوب تصريحيّ.
6. المُنتِج والمُستهلِك (producer/consumer) ── التزامن عبر الرندفو
لنطّلع على نمط المُنتِج والمُستهلِك بوصفه نمطاً كلاسيكيّاً يستخدم الرندفو.
task Consumer is
entry Deliver (Item : Integer);
end Consumer;
task Producer;
task body Consumer is
Sum : Integer := 0;
begin
for I in 1 .. 5 loop
accept Deliver (Item : Integer) do
Sum := Sum + Item;
end Deliver;
end loop;
end Consumer;
task body Producer is
begin
for I in 1 .. 5 loop
Consumer.Deliver (I);
end loop;
end Producer;
في هذا النمط (04_producer_consumer.ada)، يحدث تزامن مع المُستهلِك في كلّ مرّة يستدعي فيها المُنتِج Deliver. إذا كان المُنتِج أسرع من اللازم، يُنتظَر حتّى يستدعي المُستهلِك accept، وإذا كان المُستهلِك أسرع من اللازم، يُنتظَر حتّى الاستدعاء التالي من المُنتِج. أي أنّ ضغطاً عكسيّاً (backpressure) طبيعيّاً يُفرَض.
7. الكائن المحمي (protected object) ── إقصاء متبادل بلا قفل
في مقابل المهمّة التي هي “فاعل نشِط”، فإنّ الكائن المحمي (protected object) آليّة مخصّصة لـ”بيانات مشتركة سلبيّة”.
protected Counter is
procedure Increment;
function Value return Integer;
private
Count : Integer := 0;
end Counter;
protected body Counter is
procedure Increment is
begin
Count := Count + 1;
end Increment;
function Value return Integer is
begin
return Count;
end Value;
end Counter;
القواعد المهمّة للكائن المحمي هي كالتالي.
- الدالّة (function) للقراءة فقط. يمكن لعدّة مهامّ استدعاء الدالّة في الوقت نفسه.
- الإجراء (procedure) للقراءة والكتابة. تُحجَب أثناء تنفيذ الإجراء الإجراءات الأخرى والدوالّ أيضاً.
- المدخل (entry) مزوَّد بحاجز (barrier). ينتظر المستدعي في طابور حتّى يصبح شرط الحاجز صحيحاً.
في هذه الشيفرة (05_protected_counter.ada)، تستدعي ثلاث مهامّ عاملة (worker) كلّ منها Increment 1,000 مرّة. ولأنّ الكائن المحمي يضمن التحكّم الحصريّ، فإنّ قيمة العدّاد النهائيّة تكون دائماً 3,000. لا حاجة لكتابة قفل الميوتكس وفكّه يدويّاً.
task type Worker (Id : Integer; Rounds : Integer);
task body Worker is
begin
for I in 1 .. Rounds loop
Counter.Increment; -- الكائن المحمي يضمن الحصرية
end loop;
end Worker;
W1 : Worker (1, 1_000);
W2 : Worker (2, 1_000);
W3 : Worker (3, 1_000);
ماذا يحدث بلا كائن محمي
لفهم قيمة الكائن المحمي، لنطّلع على شيفرة خطِرة في حالة عدم الحماية.
-- ⚠ خطر: معالجة مباشرة لمتغيّر مشترك
Shared_Counter : Integer := 0;
task body Bad_Worker is
begin
for I in 1 .. 10_000 loop
Shared_Counter := Shared_Counter + 1; -- تسابق بيانات!
end loop;
end Bad_Worker;
على مستوى المعالج (CPU)، تتكوّن Shared_Counter := Shared_Counter + 1 من ثلاث خطوات: “قراءة → جمع → إعادة كتابة”. فإذا نفّذت عدّة مهامّ هذا في الوقت نفسه، فقد لا تلحق نتيجة الجمع لدى إحدى المهامّ بالقراءة التي أجرتها مهمّة أخرى، فتُفقَد بعض عمليّات الزيادة. علاوة على ذلك، يندرج هذا ضمن ما تسمّيه Ada RM 9.10 بالتنفيذ الخاطئ (erroneous execution). فالقراءة والكتابة المتزامنتان لمتغيّر مشترك غير متزامَن لا تقتصران على عدم دقّة قيمة العدّاد النهائيّة، بل قد يصبح سلوك البرنامج بأكمله عشوائيّاً. حتّى لو نفّذت مهمّتان كلّ منهما 10,000 مرّة، فليس هناك أيّ ضمان بأن تصل القيمة النهائيّة إلى 20,000.
الكائن المحمي آليّة “تمنع هذه المشكلة عبر بنية اللغة نفسها (syntax)”. فبمجرّد استدعاء Counter.Increment;، يضمن المترجم (compiler) وبيئة التشغيل (runtime) التحكّم الحصريّ.
8. المداخل المحميّة والحواجز ── المخزن المؤقّت المحدود (bounded buffer)
بإضافة مدخل (entry) إلى الكائن المحمي، يصبح التزامن الشرطيّ ممكناً. لنطّلع على المثال الكلاسيكيّ للمخزن المؤقّت المحدود.
type Buffer_Array is array (0 .. Buffer_Size - 1) of Integer;
protected Buf is
entry Put (Item : Integer);
entry Get (Item : out Integer);
private
Data : Buffer_Array;
Head : Integer := 0;
Tail : Integer := 0;
Count : Integer := 0;
end Buf;
protected body Buf is
entry Put (Item : Integer) when Count < Buffer_Size is
begin
Data (Tail) := Item;
Tail := (Tail + 1) mod Buffer_Size;
Count := Count + 1;
end Put;
entry Get (Item : out Integer) when Count > 0 is
begin
Item := Data (Head);
Head := (Head + 1) mod Buffer_Size;
Count := Count - 1;
end Get;
end Buf;
when Count < Buffer_Size هي الحاجز (barrier). يُقيَّم الحاجز عند كلّ استدعاء للمدخل: إن كان صحيحاً يُنفَّذ، وإن كان خاطئاً تنتظر المهمّة المستدعية في طابور. وفي كلّ مرّة تتغيّر حالة المخزن (تنفّذ مهمّة أخرى Put أو Get)، يُعاد تقييم حاجز المهامّ المنتظِرة.
هذا النمط (06_bounded_buffer.ada) من أبرز المواضع التي يتألّق فيها الكائن المحمي في Ada. قارنه بالكتابة بلغة C باستخدام mutex + condition variable من pthread.
// حالة لغة C + pthread (للمقارنة مع Ada)
pthread_mutex_lock(&mutex);
while (count >= BUFFER_SIZE) { // يقابل when في Ada
pthread_cond_wait(¬_full, &mutex); // انتظار الحاجز
}
data[tail] = item;
tail = (tail + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(¬_empty); // إشعار المهام المنتظرة
pthread_mutex_unlock(&mutex);
في Ada، يتلخّص كلّ هذا في سطر واحد هو when Count < Buffer_Size. شرط حلقة while، وإرسال الإشارة، وأخطاء توقيت فكّ القفل — كلّ فرص هذه الأخطاء تختفي.
9. الاستدعاء بمهلة زمنيّة ── عدم الانتظار إلى الأبد
في الأنظمة ذات الزمن الحقيقيّ، لا يُسمَح بـ”الانتظار إلى الأبد”. تدعم Ada المهلة الزمنيّة عبر بنية select ... or delay.
select
Slow_Worker.Do_Work (Result);
Put_Line ("Main: work completed");
or
delay until Ada.Real_Time.Clock + Milliseconds (500);
Put_Line ("Main: timeout after 500ms!");
end select;
في هذه الشيفرة (07_timed_entry.ada)، لأنّ Slow_Worker لا يزال في تنفيذ delay 2.0 ولم يصل بعد إلى accept، ينتهي استدعاء المدخل الذي دخل الطابور بمهلة 500 ميلي ثانية. (تسري المهلة الزمنيّة على مدّة انتظار الطابور قبل قبول الرندفو، ولا تقاطع تنفيذ جسم الرندفو نفسه.) delay until تحديد بزمن مطلق، وهي تقنيّة أساسيّة في البرمجة ذات الزمن الحقيقيّ لمنع الانجراف التراكميّ (cumulative drift).
إضافة إلى ذلك، تدعم Ada أيضاً الاستدعاء الشرطيّ (conditional entry call).
select
Server.Process (Item);
else
Put_Line ("Server is busy, will retry later");
end select;
بفضل جملة else، إذا تعذّر إجراء الرندفو فوراً، يُنتقَل مباشرةً إلى معالجة بديلة. لا حاجة لكتابة الاستقصاء (polling) يدويّاً.
لا تنسَ تصميم ما بعد المهلة الزمنيّة
المهلة الزمنيّة مفيدة، لكنّ جوهر التصميم هو “ماذا نفعل بعد تعذّر الانتظار؟”. هل يجوز التخلّي عن القيمة فعلاً؟ هل ينبغي إعادة المحاولة؟ هل يجب الإبلاغ بها كخطأ إلى مستوى أعلى؟ — إن تُرِك هذا غامضاً، يتحوّل الأمر في بيئة الإنتاج إلى فقدان بيانات أو توقّف خدمة. عند كتابة مهلة زمنيّة، صمِّم مسؤوليّة ما بعدها في المكان نفسه.
المهامّ الدوريّة و delay until
لا تُستخدَم delay until للمهلة الزمنيّة فقط، بل أيضاً للتنفيذ الدوريّ. ففي حين تجعل delay 0.1 البسيطة الدورة تساوي “زمن المعالجة + 0.1 ثانية”، فإنّ delay until تحدّد نقطة التشغيل التالية بزمن مطلق، فتحافظ على دورة ثابتة لا تتأثّر بزمن المعالجة.
loop
Next := Next + Period;
Do_Work;
delay until Next;
end loop;
هذا النمط فعّال في كلّ موضع يتطلّب معالجة دوريّة ثابتة، مثل مراقبة المستشعرات (sensors) أو حلقات التحكّم.
10. أولويّة المهامّ والجدولة ذات الزمن الحقيقيّ
مزايا Ada ذات الزمن الحقيقيّ مُعرَّفة في الملحق Annex D (Real-Time Systems). إذا كان مترجم Ada يدعم Annex D، يمكن تحديد أولويّة المهامّ وسياسة الجدولة.
task High_Task is
pragma Priority (System.Default_Priority + 5);
end High_Task;
task Low_Task is
pragma Priority (System.Default_Priority);
end Low_Task;
كإعداد أكثر تقدّماً، يمكن أيضاً تحديد سياسة الجدولة وبروتوكول سقف الأولويّة.
pragma Task_Dispatching_Policy (FIFO_Within_Priorities);
pragma Locking_Policy (Ceiling_Locking);
بروتوكول سقف الأولويّة (Priority Ceiling Protocol) بروتوكول يمنع انعكاس الأولويّة. يُحدَّد لكلّ كائن محمي صراحةً أولويّة السقف (ceiling priority) عبر pragma Priority (أو سمة Priority). وإذا تجاوزت الأولويّة الفعّالة (active priority) للمهمّة المستدعية ذلك السقف، يحدث Program_Error. وأثناء قفل الكائن، يُنفَّذ التنفيذ بأولويّة السقف، ما يمنع المهامّ متوسّطة الأولويّة من تجاوزه (preemption).
protected Shared_Data is
pragma Priority (15); -- أولوية السقف
procedure Update (Val : Integer);
function Read return Integer;
private
Data : Integer := 0;
end Shared_Data;
تستند هذه الميزات إلى الخلفيّة النظريّة لـجدولة Rate Monotonic (RMS)، ولها سجلّ حافل في أنظمة الزمن الحقيقيّ الصارم (hard real-time)، مثل التحكّم في رحلات الطائرات والأجهزة الطبّية.
11. إرشادات تصميميّة للتطبيق العمليّ
استعرضنا حتّى الآن البنية الأساسيّة للمهامّ والكائنات المحميّة. أخيراً، نلخّص الإرشادات التصميميّة التي ينبغي مراعاتها عند استخدام التزامن في Ada عمليّاً.
ما لا يجوز فعله داخل الكائن المحمي
القاعدة الذهبيّة داخل الكائن المحمي هي الاكتفاء بتحديث الحالة بإيجاز، وتنفيذ المعالجة الثقيلة خارجه. لأنّ عمليّات الكائن المحمي محكومة داخليّاً بتحكّم حصريّ، فإنّ الحجب لفترة طويلة داخلها يوقف كلّ المهامّ الأخرى التي تستخدم الكائن المحمي نفسه.
المعالجات التي ينبغي تجنّبها تحديداً:
delayأو عمليّات I/O بطيئة- استدعاءات معقّدة لكائن محمي آخر
- استدعاءات ثقيلة لمكتبات خارجيّة
إضافة إلى ذلك، فإنّ استخدام delay أو بعض عمليّات I/O داخل عمليّة محميّة ليس مجرّد مشكلة أداء، بل هو خطأ محدود (bounded error) حسب مواصفة Ada. وقد يؤدّي حسب المترجم إلى Program_Error أو إلى الوقوع في توقّف تامّ، لذا يجب استبعاده تماماً لا مجرّد “التقليل منه”.
التصميم الجيّد هو النمط التالي: “استخراج القيمة المطلوبة من الكائن المحمي بسرعة ← إجراء الحساب الثقيل أو I/O خارجه ← إعادة كتابة النتيجة فقط بسرعة إلى الكائن المحمي”.
حافظ على بساطة شروط الحاجز
حاجز entry ... when <condition> قويّ، لكن إذا أصبح معقّداً أكثر من اللازم يصعب قراءته، ويصعب معرفة سبب عدم تحرّر المهمّة.
المستوى المثاليّ هو أن يكون معنى الحالة مفهوماً بنظرة واحدة، كما في when Count < Buffer_Size أو when Used > 0. وإذا لزمت عدّة شروط، فكِّر في تمثيل الحالة بنوع تعدادي (enumeration)، وتقريب الحاجز من شكل يُقرَأ باسم الحالة، مثل when State = Running.
استثناءات المهامّ وتوقّفها
يجب تحديد السياسة الخاصّة بحدوث استثناء داخل المهمّة بوضوح مسبقاً. الحدّ الأدنى هو التقاط الاستثناء في أعلى مستوى من جسم المهمّة، وتسجيل ما حدث.
والأهمّ من ذلك هو التصميم لما بعد الاستثناء. هل يستطيع النظام الاستمرار إذا توقّفت تلك المهمّة؟ هل يجوز إعادة تشغيلها؟ كيف يُبلَّغ بذلك إلى المهامّ الأخرى؟ كيف تُعاد الحالة المشتركة إلى وضع آمن؟ — يجب أن تكون الإجابة عن هذه الأسئلة جاهزة. تملك Ada آليّة استثناءات كلغة، لكنّ سلامة ما بعد الاستثناء مسؤوليّة تصميم التطبيق.
قائمة تدقيق تصميميّة مصغّرة
| الجانب | ما ينبغي التحقّق منه |
|---|---|
| الحالة المشتركة | هل هي محصورة داخل كائن محمي؟ هل تُلمَس مباشرةً من الخارج؟ |
| العمليّة المحميّة | هل هي قصيرة؟ هل تحجب في داخلها؟ |
| المدخل | هل الحاجز بسيط؟ هل هناك احتمال انتظار إلى الأبد؟ هل هناك سياسة مهلة زمنيّة؟ |
| عمر المهمّة | هل شرط الانتهاء واضح؟ هل هناك سياسة عند الاستثناء؟ |
| المعالجة الدوريّة | هل جرى النظر في استخدام delay until بدلاً من delay؟ |
في التزامن، أخطر شيء هو “ربّما يكون الأمر على ما يرام”. تصريح الحالة المشتركة وشروط الانتظار وشروط الانتهاء وسياسة الاستثناءات بوضوح في الشيفرة هو الخطوة الأولى نحو تزامن آمن.
12. الخلاصة ── لغة جعلت التزامن “قواعد نحويّة”
ما يميّز نموذج التزامن في Ada عن اللغات الأخرى هو أنّ التزامن الآمن مدمج كـ”قواعد نحويّة” لا كـ”أفضل ممارسات مُلحَقة لاحقاً”.
| ما تريد فعله | بنية Ada |
|---|---|
| وحدة تنفيذ مستقلّة | task / task body |
| تواصل متزامن | entry / accept |
| انتظار عدّة طلبات | select / or / else |
| تحكّم حصريّ | protected / function / procedure |
| تزامن شرطيّ | entry ... when <barrier> |
| مهلة زمنيّة | or delay until <time> |
| التحكّم بالأولويّة | pragma Priority |
هذه البنى خاضعة للتحقّق من قِبَل المترجم (compiler). فمثلاً، محاولة تعديل عنصر خاصّ (private) تابع للكائن المحمي نفسه داخل دالّة الكائن المحمي تُسبِّب خطأ ترجمة (compile error). وعند اكتمال العمليّة المحميّة، يُعاد تقييم حاجز المداخل المنتظِرة تلقائيّاً — لا حاجة لإرسال إشارة يدويّاً.
"كما يضمن نظام الأنواع سلامة الذاكرة،
تضمن بنية التزامن في Ada سلامة التزامن"
أمثلة الشيفرة الثمانية التي تناولها هذا المقال مدخل عمليّ إلى المهامّ والرندفو والكائنات المحميّة وميزات الزمن الحقيقيّ. وأثناء تجربتها بنفسك، حاول أيضاً خوض المواضيع المتقدّمة التالية.
- ملف Ravenscar: ملف تقييد للمهامّ مخصّص لأنظمة الزمن الحقيقيّ عالية الموثوقيّة. يتيح نموذج المهامّ المقيَّد تحليلاً ثابتاً (static) للتوقّف التامّ.
- الكتل المتوازية في Ada 2022: معالجة متوازية للبيانات عبر بنية
parallel ... do. - التكامل مع SPARK: التحقّق الشكليّ من سلوك البرامج المتزامنة (مدعوم عبر GNATprove ضمن ملف Ravenscar).
ومع ذلك، ليس “استخدام Ada يعني الأمان” بالضرورة
تنبيه مهمّ أخير. بنية التزامن في Ada قويّة، لكنّ استخدام Ada لا يجعل البرنامج آمناً تلقائيّاً. لمس بيانات مشتركة مباشرةً دون وضعها في كائن محمي، أو الحجب لفترة طويلة داخل كائن محمي، أو استدعاء عدّة كائنات محميّة بعضها بعضاً بتعقيد — أخطاء تصميميّة كهذه يمكن أن تحدث حتّى في Ada.
ميزات اللغة مصمَّمة بحيث “تتطلّب الكتابة الخطِرة جهداً واضحاً”، لكنّها لا تقوم بالتصميم الصحيح نفسه بالنيابة عنك. القيمة الحقيقيّة لـ Ada هي إمكان تقريب النقاش حول السلامة إلى مكان قريب من الشيفرة — أي إمكان ترك أسئلة مثل “هل هذه الحالة محميّة؟” و”متى تنتهي هذه المهمّة؟” و”بأيّ شرط ينتظر هذا المدخل؟” على هيئة بنية نحويّة في الشيفرة نفسها.
فلسفة Ada التي تعبّر عن التصميم عبر الأنواع (types) متّسقة أيضاً في التزامن. لا يبدأ التزامن الآمن بالتعامل الحذِر مع الأقفال، بل بعدم ترك الحالة المشتركة الخطِرة عارية دون حماية.
في مواجهة الفكرة الشائعة “التزامن صعب”، تجيب Ada بأنّه “إن اخترت البنية الصحيحة، يضمن المترجم السلامة”. وهذه الفلسفة التصميميّة تتّصل بلغات حديثة مثل Rust وPony، إلّا أنّ Ada تحملها كجزء من مواصفة اللغة منذ 40 عاماً.
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
برمجة أنظمة الوقت الفعليّ بلغة Ada ── التطبيق العمليّ للتحكّم في الأولويّة والدوريّة ووقت التنفيذ
نتعلّم الملحق D (أنظمة الوقت الفعليّ) في Ada عبر ثمانية أمثلة برمجيّة تطبيقيّة. نرتِّب تدريجيّاً أولويّة المهامّ، وبروتوكول Ceiling_Locki...
البرمجة العامّة (Generics) في Ada ── كتابة العقود بالأنواع، وتحقيق إعادة الاستخدام دون تكلفة إضافيّة
شرح شامل للبرمجة العامّة (generic programming) في Ada، من الإجراءات العامّة، والحزم العامّة، والمعطيات الفرعيّة (formal subprograms)، وفئ...
مدخل إلى التحقق الشكلي باستخدام SPARK ── من عقود Ada إلى البرهان الرياضي
مقال تطبيقيّ يقدّم مدخلاً إلى التحقق الشكلي باستخدام SPARK، مجموعة الأداة الفرعيّة للغة Ada. يتناول المقال الانتقال من العقود (Pre/Post) ...
سحر لغة Ada ── لغة تُعبِّر عن التصميم بالأنواع، وتدعم برمجيّات تعمل لعقود
نقدّم سحر لغة Ada. الأنواع القويّة، وقيود المدى، والفصل بين المواصفة والتنفيذ عبر الحزم، والتصميم بالعقد، والمهامّ المدمجة في اللغة، والت...
معالجة الأخطاء وتصميم إعادة المحاولة في Power Automate ── منع «توقّف تدفّق كان يعمل دون أن يلاحظ أحد»
مجموعة أنماط تصميميّة لمنع تدفّقات Power Automate من «التوقّف دون أن يلاحظ أحد». نستعرض، استناداً إلى مواصفات Microsoft Learn، القيم الاف...
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- ما هي المهمّة (task) في Ada؟
- المهمّة هي وحدة التزامن الأساسية في Ada، وتشبه الخيط (thread) لكنّها لا تقابل بالضرورة خيط نظام التشغيل واحداً بواحد، إذ تتولّى بيئة تشغيل Ada (runtime) إدارة الجدولة. تبدأ المهمّة تنفيذها تلقائيّاً بمجرّد الإعلان عنها، وعندما ينتهي الإجراء الرئيسي (main procedure) يُنتظَر ضمنيّاً اكتمال المهامّ التي لا تزال قيد التنفيذ. يجري التواصل المتزامن مع الخارج عبر رندفو (rendezvous) من خلال مدخل (entry). المهامّ والرندفو موجودان في مواصفات اللغة منذ Ada 83 عام 1983.
- ما الفرق بين الكائن المحمي (protected object) في Ada والميوتكس (mutex)؟
- الكائن المحمي هو آلية إقصاء متبادل تديرها اللغة نفسها، فلا تحتاج إلى كتابة القفل وفكّه (lock/unlock) يدويّاً. الدالّة (function) للقراءة فقط ويمكن لعدّة مهامّ استدعاءها في الوقت نفسه، والإجراء (procedure) للقراءة والكتابة وتُحجَب أثناء تنفيذه أي استدعاءات أخرى، أمّا المدخل (entry) فيُبقي المستدعي منتظِراً في طابور حتّى يصبح شرط الحاجز (barrier) صحيحاً. التحكّم في المخزن المؤقّت المحدود الذي يُكتَب بلغة C بدمج mutex ومتغيّر شرطي (condition variable) من pthread، يتلخّص في Ada بسطر حاجز واحد مثل `when Count < Buffer_Size`.
- ما هي آلية الرندفو (rendezvous) في Ada؟
- الرندفو آلية تواصل متزامن بين المهامّ، حيث ينتظر كلّ من استدعاء المدخل من جهة المستدعي وجملة accept من جهة المهمّة بعضهما بعضاً حتّى يصلا معاً إلى نقطة الرندفو. يمكن تمرير البيانات في الاتّجاهين عبر أنماط المعاملات in وout وin out. تُشكِّل الكتلة do~end داخل جسم accept قسماً حرجاً (critical section)؛ فأثناء تنفيذها يُحجَب المستدعي ولا تقبل المهمّة أيّ مدخل آخر. وبدمج ذلك مع جملة select يمكن كتابة انتظار عدّة مداخل في آن واحد، والمهلة الزمنيّة، وشروط الحرس (guard conditions) بأسلوب تصريحيّ (declarative).
- ما الذي يُمنع فعله داخل الكائن المحمي؟
- أيّ معالجة تحجب لوقت طويل، مثل delay أو عمليّات إدخال/إخراج (I/O) بطيئة أو استدعاءات ثقيلة لمكتبات خارجيّة. يندرج استخدام delay أو بعض عمليّات I/O داخل عمليّة محمية ضمن ما تسمّيه مواصفة Ada بالخطأ المحدود (bounded error)، وقد يؤدّي حسب المترجم (implementation) إلى ظهور Program_Error أو إلى توقّف تام (deadlock)، لذا يجب استبعاده تماماً لا مجرّد تجنّبه. القاعدة الذهبيّة هي الاكتفاء بتحديث الحالة بإيجاز داخل الكائن المحمي، وتنفيذ الحسابات الثقيلة وعمليّات I/O خارجه، ثمّ إعادة كتابة النتيجة فقط بسرعة.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة