Semaphore هو أداة مزامنة تتحكم في الوصول إلى مورد مشترك من خلال عداد وطابور من الخيوط المنتظرة. وفقاً لـ Wikipedia, 2024، تم اقتراح السيمافور بواسطة إدسخر دايكسترا في عام 1965 لحل مشاكل التفاعل متعدد الخيوط. تتيح الأداة تحديد عدد الخيوط التي تعمل في وقت واحد مع مقطع حرج.
الخلاصة
Semaphore هو أداة مزامنة تستخدم عداداً للتحكم في الوصول إلى مورد مشترك. تم اقتراح المفهوم بواسطة إدسخر دايكسترا في عام 1965 وأصبح الأساس لجميع آليات المزامنة الحديثة في أنظمة التشغيل.
السيمافور هو متغير صحيح مع عمليتين ذريتين: wait (acquire) و signal (release). عملية wait تقلل العداد، بينما signal تزيده. عندما يصل العداد إلى الصفر، يتم حظر الخيط الذي يستدعي wait حتى ينفذ خيط آخر signal.
الغرض الرئيسي من السيمافور هو حماية المقاطع الحرجة من الوصول المتزامن من قبل خيوط متعددة. على عكس الميوتكس، لا يتطلب السيمافور الارتباط بخيط مالك، مما يجعله مناسباً لمجموعة أوسع من مهام التنسيق.
نشأ مفهوم السيمافور في سياق نظام التشغيل THE، الذي تم تطويره في Technische Hogeschool Eindhoven. قام دايكسترا بإضفاء الطابع الرسمي على السيمافور كتجريد رياضي، مثبتاً كفايته لتنفيذ أي أدوات مزامنة.
آلية السيمافور تعتمد على عمليتين ذريتين وطابور انتظار داخلي. عند استدعاء acquire، يتحقق الخيط من قيمة العداد وإما يواصل التنفيذ أو يتم حظره حتى يتم تحرير المورد.
عند إنشاء السيمافور، يتم تعيين قيمة أولية لعداد التصاريح. كل استدعاء acquire يقلل العداد بمقدار 1. إذا أصبح العداد سالباً بعد ذلك، يتم حظر الخيط. عملية release تزيد العداد وتوقظ أحد الخيوط المنتظرة.
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} يعمل")
} finally {
semaphore.release()
}
}
عندما يستدعي خيط acquire وعداد يساوي صفراً، يضعه نظام التشغيل في طابور FIFO للسيمافور. ينتقل الخيط إلى الحالة BLOCKED، دون استهلاك وقت CPU. بعد استدعاء release، ينتقل أول خيط في الطابور إلى الحالة RUNNABLE ويحصل على الوصول إلى المورد.
في نظرية المزامنة، يتم تمييز نوعين رئيسيين من السيمافورات: الثنائي والعددي. يعتمد اختيار النوع على المهمة المحددة لإدارة الوصول إلى الموارد.
السيمافور الثنائي يأخذ فقط القيمتين 0 و1. في سلوكه، يشبه الميوتكس، ولكن بدون شرط الملكية — يمكن لأي خيط تنفيذ release. هذه السيمافورات مناسبة لتنفيذ علامات الجاهزية والأحداث بين الخيوط.
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("البيانات جاهزة")
}
السيمافور العددي يمكن أن يأخذ أي قيمة غير سالبة. يُستخدم لإدارة مجموعة من الموارد المتشابهة حيث تتوفر عدة نسخ. على سبيل المثال، مجموعة من 5 اتصالات شبكة: كل acquire يأخذ اتصالاً واحداً، و release يعيده إلى المجموعة.
السيمافورات العددية لا غنى عنها للحد من سرعة الوصول إلى الخدمات الخارجية وتنفيذ مجموعات الخيوط. تسمح بالتحكم الدقيق في درجة التوازي دون إدارة يدوية للخيوط.
| المعامل | السيمافور الثنائي | السيمافور العددي |
|---|---|---|
| النطاق | 0 أو 1 | من 0 إلى N |
| الخيوط المتزامنة | 1 | حتى N |
| التطبيق | الإشارات، العلامات | مجموعات الموارد، تحديد السرعة |
غالباً ما يخلط المطورون بين السيمافور والميوتكس، على الرغم من وجود اختلافات جوهرية بينهما. فهم هذه الاختلافات مهم جداً لاختيار آلية المزامنة الصحيحة في المشروع.
الاختلاف الرئيسي هو مفهوم الملكية. يعرف الميوتكس دائماً أي خيط استحوذ عليه، ويمكن لهذا الخيط فقط تحريره. السيمافور ليس له مالك: يمكن لأي خيط استدعاء release دون حتى استدعاء acquire. هذا يجعل الميوتكس أكثر أماناً لحماية البيانات والسيمافور أكثر مرونة للتنسيق.
عملياً، الميوتكس أسرع للاستبعاد المتبادل البسيط بفضل التحسينات للسيناريوهات النموذجية. يتطلب السيمافور تكاليف إضافية للحفاظ على العداد. ومع ذلك، لتحديد التوازي أو تنفيذ نمط المنتج-المستهلك، لا غنى عن السيمافور.
| الخاصية | Semaphore | Mutex |
|---|---|---|
| الملكية | لا مالك | له مالك |
| التحرير | أي خيط | فقط الخيط المالك |
| العداد | من 0 إلى N | ثنائي |
| حالة الاستخدام | تحديد التوازي والإشارات | حماية المقطع الحرج |
| العودية | لا | نعم (قابل لإعادة الدخول) |
في تطوير التطبيقات المحمولة، يُستخدم Semaphore للتحكم في الوصول إلى الموارد المحدودة: اتصالات الشبكة، الملفات، قواعد البيانات والمكونات المادية. توفر المنصات الحديثة تطبيقات مدمجة ملائمة.
إحدى حالات الاستخدام النموذجية هي مجموعة اتصالات HTTP. يمكن للتطبيق إرسال ما لا يزيد عن 4 طلبات متزامنة إلى الخادم لأن API المزود يحد من التوازي. يضمن السيمافور بقيمة ابتدائية 4 أنه تحت أي حمل، لا يتجاوز عدد الطلبات المتزامنة الحد، بينما تنتظر الخيوط الأخرى في الطابور.
بدون السيمافور، قد تؤدي الزيادة الحادة في نشاط المستخدم إلى حمل زائد مفاجئ على البنية التحتية للخادم، مما يؤدي إلى انتهاء المهلة وأخطاء 429 Too Many Requests. يعمل Semaphore كمصهر، يسمح بعدد محدد بدقة من الاستدعاءات المتزامنة بغض النظر عن عدد الخيوط النشطة.
يوفر Android فئة Semaphore من حزمة java.util.concurrent. دعنا نرى مثالاً لتحديد طلبات الشبكة المتزامنة إلى خيطين لمنع تحميل الخادم الزائد.
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
في iOS، يحل DispatchSemaphore من GCD نفس المهمة. يستخدمه المطورون لمزامنة الوصول إلى الموارد في الكود غير المتزامن دون حظر الخيط الرئيسي.
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
الخطأ الأكثر شيوعاً هو release المنسي عند حدوث استثناء. إذا انتهى خيط بخطأ قبل استدعاء release، يبقى السيمافور محظوراً بشكل دائم للخيوط الأخرى. استخدم try/finally أو defer لضمان التحرير. المشكلة الثانية هي deadlock عند استحواذ عدة سيمافورات بترتيب مختلف من قبل خيوط مختلفة.
لا تُستخدم السيمافورات فقط لحماية البيانات، ولكن أيضاً لتنسيق الخيوط في سيناريوهات متعددة الخيوط المعقدة. معرفة الأنماط الشائعة تسرع التطوير وتقلل من احتمالية أخطاء المزامنة.
هناك عدة أنماط مجربة لاستخدام السيمافورات في المشاريع الحقيقية. معرفتها تساعد في تجنب الأخطاء النموذجية وبناء أنظمة متعددة الخيوط موثوقة.
سيمافور بقيمة ابتدائية N وتحرير دوري عبر مؤقت ينفذ تحديد السرعة لطلبات API. مثلاً، خدمة تسمح بـ 10 طلبات في الثانية: يبدأ السيمافور من 10، كل طلب يقلل العداد، و TimerTask منفصل يعيد العداد إلى قيمته الابتدائية كل ثانية. هذا يحمي التطبيق والخادم من الحمل الزائد.
في مشكلة المنتج-المستهلك الكلاسيكية، سيمافوران يديران مخزناً: empty (تصاريح الكتابة) و full (تصاريح القراءة). المنتج يستدعي acquire على empty و release على full، والمستهلك يفعل العكس. هذا المخطط يضمن أن المستهلك لا يقرأ أبداً مخزناً فارغاً والمنتج لا يفيضه أبداً.
هذا المخطط نفسه هو الأساس للمخزن المحدود في أنظمة التشغيل — مخزن حلقي بحجم ثابت. في التطبيقات المحمولة، يُستخدم النمط لمعالجة طوابير الصور وملفات الفيديو وأحداث التحليلات.
تُستخدم السيمافورات بنجاح لتحديد استدعاءات الشبكة في الخدمات الخلفية. مثلاً، تطبيق تحليلات يرسل حزم أحداث إلى الخادم. بدون تحديد الخيوط المتزامنة أثناء الأحمال القصوى (تشغيل التطبيق، المزامنة بعد عدم الاتصال)، قد يتجاوز عدد الطلبات المتزامنة حدود الخادم. سيمافور بقيمة ابتدائية 3 يضمن إرسالاً سلساً ويمنع الحظر من جانب الخادم.
الأسئلة الشائعة
Semaphore ليس مجرد عداد، بل أداة مزامنة مع عمليات ذرية وطابور انتظار. العداد العادي لا يحظر الخيط ولا يضمن ذرية الزيادة عند الوصول المتزامن من خيوط متعددة.
نعم، deadlock ممكن عند الاستحواذ على عدة سيمافورات بترتيب مختلف من قبل خيوط مختلفة. مثلاً، الخيط A يستحوذ على S1 ثم S2، بينما الخيط B يستحوذ على S2 ثم S1. حدد ترتيب استحواذ واحد لجميع السيمافورات في المشروع.
يتم حظر الخيط ويدخل في حالة الانتظار. لا يستهلك وقت CPU حتى يستدعي خيط آخر release. في Java، هذه هي الحالة BLOCKED؛ في Swift، يتم تعليق الخيط بواسطة GCD.
الفرق الرئيسي هو الملكية. يمكن تحرير Mutex فقط من قبل الخيط المالك. يمكن تحرير Binary Semaphore من قبل أي خيط، وهو مناسب للإشارة بين الخيوط ولكنه أقل أماناً لحماية سلامة البيانات.
القيمة الابتدائية تعتمد على السيناريو. لحماية مورد واحد — 1. لمجموعة من N اتصالاً — N. للإشارة بين الخيوط، استخدم 0 بحيث ينتظر خيط المستهلك إشارة من المنتج.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا