Semaphore: ما هو، مبدأ عمله وتطبيقه في مزامنة الخيوط

المؤلف: IT Sectr نُشر: 2026-03-19 وقت القراءة: 8 دق

Semaphore هو أداة مزامنة تتحكم في الوصول إلى مورد مشترك من خلال عداد وطابور من الخيوط المنتظرة. وفقاً لـ Wikipedia, 2024، تم اقتراح السيمافور بواسطة إدسخر دايكسترا في عام 1965 لحل مشاكل التفاعل متعدد الخيوط. تتيح الأداة تحديد عدد الخيوط التي تعمل في وقت واحد مع مقطع حرج.

الخلاصة

  • Semaphore هو أداة مزامنة تتحكم في الوصول من خلال عداد التصاريح.
  • السيمافور الثنائي يأخذ القيمتين 0 و1، ويعمل كعلامة حظر.
  • السيمافور العددي يسمح بالوصول المتزامن لعدد محدد من الخيوط.
  • على عكس الميوتكس، السيمافور غير مرتبط بخيط مالك.
  • Deadlock هو أحد المخاطر الرئيسية عند استخدام السيمافورات بشكل غير صحيح.

ما هو Semaphore؟

Semaphore هو أداة مزامنة تستخدم عداداً للتحكم في الوصول إلى مورد مشترك. تم اقتراح المفهوم بواسطة إدسخر دايكسترا في عام 1965 وأصبح الأساس لجميع آليات المزامنة الحديثة في أنظمة التشغيل.

التعريف والغرض

السيمافور هو متغير صحيح مع عمليتين ذريتين: wait (acquire) و signal (release). عملية wait تقلل العداد، بينما signal تزيده. عندما يصل العداد إلى الصفر، يتم حظر الخيط الذي يستدعي wait حتى ينفذ خيط آخر signal.

الغرض الرئيسي من السيمافور هو حماية المقاطع الحرجة من الوصول المتزامن من قبل خيوط متعددة. على عكس الميوتكس، لا يتطلب السيمافور الارتباط بخيط مالك، مما يجعله مناسباً لمجموعة أوسع من مهام التنسيق.

التاريخ والأساس النظري

نشأ مفهوم السيمافور في سياق نظام التشغيل THE، الذي تم تطويره في Technische Hogeschool Eindhoven. قام دايكسترا بإضفاء الطابع الرسمي على السيمافور كتجريد رياضي، مثبتاً كفايته لتنفيذ أي أدوات مزامنة.

كيف يعمل السيمافور؟

آلية السيمافور تعتمد على عمليتين ذريتين وطابور انتظار داخلي. عند استدعاء acquire، يتحقق الخيط من قيمة العداد وإما يواصل التنفيذ أو يتم حظره حتى يتم تحرير المورد.

العداد والعمليات الذرية

عند إنشاء السيمافور، يتم تعيين قيمة أولية لعداد التصاريح. كل استدعاء acquire يقلل العداد بمقدار 1. إذا أصبح العداد سالباً بعد ذلك، يتم حظر الخيط. عملية release تزيد العداد وتوقظ أحد الخيوط المنتظرة.

kotlin
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 ويحصل على الوصول إلى المورد.

أنواع السيمافورات

في نظرية المزامنة، يتم تمييز نوعين رئيسيين من السيمافورات: الثنائي والعددي. يعتمد اختيار النوع على المهمة المحددة لإدارة الوصول إلى الموارد.

السيمافور الثنائي (Binary Semaphore)

السيمافور الثنائي يأخذ فقط القيمتين 0 و1. في سلوكه، يشبه الميوتكس، ولكن بدون شرط الملكية — يمكن لأي خيط تنفيذ release. هذه السيمافورات مناسبة لتنفيذ علامات الجاهزية والأحداث بين الخيوط.

kotlin
val ready = Semaphore(0)

fun producer() {
    Thread.sleep(1000)
    ready.release()
}

fun consumer() {
    ready.acquire()
    println("البيانات جاهزة")
}

السيمافور العددي (Counting Semaphore)

السيمافور العددي يمكن أن يأخذ أي قيمة غير سالبة. يُستخدم لإدارة مجموعة من الموارد المتشابهة حيث تتوفر عدة نسخ. على سبيل المثال، مجموعة من 5 اتصالات شبكة: كل acquire يأخذ اتصالاً واحداً، و release يعيده إلى المجموعة.

السيمافورات العددية لا غنى عنها للحد من سرعة الوصول إلى الخدمات الخارجية وتنفيذ مجموعات الخيوط. تسمح بالتحكم الدقيق في درجة التوازي دون إدارة يدوية للخيوط.

المعاملالسيمافور الثنائيالسيمافور العددي
النطاق0 أو 1من 0 إلى N
الخيوط المتزامنة1حتى N
التطبيقالإشارات، العلاماتمجموعات الموارد، تحديد السرعة

Semaphore ضد Mutex

غالباً ما يخلط المطورون بين السيمافور والميوتكس، على الرغم من وجود اختلافات جوهرية بينهما. فهم هذه الاختلافات مهم جداً لاختيار آلية المزامنة الصحيحة في المشروع.

مبدأ الملكية

الاختلاف الرئيسي هو مفهوم الملكية. يعرف الميوتكس دائماً أي خيط استحوذ عليه، ويمكن لهذا الخيط فقط تحريره. السيمافور ليس له مالك: يمكن لأي خيط استدعاء release دون حتى استدعاء acquire. هذا يجعل الميوتكس أكثر أماناً لحماية البيانات والسيمافور أكثر مرونة للتنسيق.

الأداء وحالات الاستخدام

عملياً، الميوتكس أسرع للاستبعاد المتبادل البسيط بفضل التحسينات للسيناريوهات النموذجية. يتطلب السيمافور تكاليف إضافية للحفاظ على العداد. ومع ذلك، لتحديد التوازي أو تنفيذ نمط المنتج-المستهلك، لا غنى عن السيمافور.

الخاصيةSemaphoreMutex
الملكيةلا مالكله مالك
التحريرأي خيطفقط الخيط المالك
العدادمن 0 إلى Nثنائي
حالة الاستخدامتحديد التوازي والإشاراتحماية المقطع الحرج
العوديةلانعم (قابل لإعادة الدخول)

استخدام Semaphore في التطوير المحمول

في تطوير التطبيقات المحمولة، يُستخدم Semaphore للتحكم في الوصول إلى الموارد المحدودة: اتصالات الشبكة، الملفات، قواعد البيانات والمكونات المادية. توفر المنصات الحديثة تطبيقات مدمجة ملائمة.

مجموعة اتصالات الخادم

إحدى حالات الاستخدام النموذجية هي مجموعة اتصالات HTTP. يمكن للتطبيق إرسال ما لا يزيد عن 4 طلبات متزامنة إلى الخادم لأن API المزود يحد من التوازي. يضمن السيمافور بقيمة ابتدائية 4 أنه تحت أي حمل، لا يتجاوز عدد الطلبات المتزامنة الحد، بينما تنتظر الخيوط الأخرى في الطابور.

بدون السيمافور، قد تؤدي الزيادة الحادة في نشاط المستخدم إلى حمل زائد مفاجئ على البنية التحتية للخادم، مما يؤدي إلى انتهاء المهلة وأخطاء 429 Too Many Requests. يعمل Semaphore كمصهر، يسمح بعدد محدد بدقة من الاستدعاءات المتزامنة بغض النظر عن عدد الخيوط النشطة.

Semaphore في Kotlin لنظام Android

يوفر Android فئة Semaphore من حزمة java.util.concurrent. دعنا نرى مثالاً لتحديد طلبات الشبكة المتزامنة إلى خيطين لمنع تحميل الخادم الزائد.

kotlin
class ApiClient {
    private val throttle = Semaphore(2)

    suspend fun fetch(url: String): Result {
        throttle.acquire()
        return try {
            httpGet(url)
        } finally {
            throttle.release()
        }
    }
}

DispatchSemaphore في Swift لنظام iOS

في iOS، يحل DispatchSemaphore من GCD نفس المهمة. يستخدمه المطورون لمزامنة الوصول إلى الموارد في الكود غير المتزامن دون حظر الخيط الرئيسي.

swift
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 عند استحواذ عدة سيمافورات بترتيب مختلف من قبل خيوط مختلفة.

أنماط استخدام السيمافورات

لا تُستخدم السيمافورات فقط لحماية البيانات، ولكن أيضاً لتنسيق الخيوط في سيناريوهات متعددة الخيوط المعقدة. معرفة الأنماط الشائعة تسرع التطوير وتقلل من احتمالية أخطاء المزامنة.

هناك عدة أنماط مجربة لاستخدام السيمافورات في المشاريع الحقيقية. معرفتها تساعد في تجنب الأخطاء النموذجية وبناء أنظمة متعددة الخيوط موثوقة.

محدد السرعة (Rate Limiter)

سيمافور بقيمة ابتدائية N وتحرير دوري عبر مؤقت ينفذ تحديد السرعة لطلبات API. مثلاً، خدمة تسمح بـ 10 طلبات في الثانية: يبدأ السيمافور من 10، كل طلب يقلل العداد، و TimerTask منفصل يعيد العداد إلى قيمته الابتدائية كل ثانية. هذا يحمي التطبيق والخادم من الحمل الزائد.

منتج-مستهلك عبر السيمافورات

في مشكلة المنتج-المستهلك الكلاسيكية، سيمافوران يديران مخزناً: empty (تصاريح الكتابة) و full (تصاريح القراءة). المنتج يستدعي acquire على empty و release على full، والمستهلك يفعل العكس. هذا المخطط يضمن أن المستهلك لا يقرأ أبداً مخزناً فارغاً والمنتج لا يفيضه أبداً.

هذا المخطط نفسه هو الأساس للمخزن المحدود في أنظمة التشغيل — مخزن حلقي بحجم ثابت. في التطبيقات المحمولة، يُستخدم النمط لمعالجة طوابير الصور وملفات الفيديو وأحداث التحليلات.

تحديد طلبات الشبكة

تُستخدم السيمافورات بنجاح لتحديد استدعاءات الشبكة في الخدمات الخلفية. مثلاً، تطبيق تحليلات يرسل حزم أحداث إلى الخادم. بدون تحديد الخيوط المتزامنة أثناء الأحمال القصوى (تشغيل التطبيق، المزامنة بعد عدم الاتصال)، قد يتجاوز عدد الطلبات المتزامنة حدود الخادم. سيمافور بقيمة ابتدائية 3 يضمن إرسالاً سلساً ويمنع الحظر من جانب الخادم.

الأسئلة الشائعة

ما الفرق بين Semaphore والعداد العادي؟

Semaphore ليس مجرد عداد، بل أداة مزامنة مع عمليات ذرية وطابور انتظار. العداد العادي لا يحظر الخيط ولا يضمن ذرية الزيادة عند الوصول المتزامن من خيوط متعددة.

هل يمكن أن يسبب السيمافور deadlock؟

نعم، deadlock ممكن عند الاستحواذ على عدة سيمافورات بترتيب مختلف من قبل خيوط مختلفة. مثلاً، الخيط A يستحوذ على S1 ثم S2، بينما الخيط B يستحوذ على S2 ثم S1. حدد ترتيب استحواذ واحد لجميع السيمافورات في المشروع.

ماذا يحدث عند استدعاء acquire مع عداد صفري؟

يتم حظر الخيط ويدخل في حالة الانتظار. لا يستهلك وقت CPU حتى يستدعي خيط آخر release. في Java، هذه هي الحالة BLOCKED؛ في Swift، يتم تعليق الخيط بواسطة GCD.

كيف يختلف Binary Semaphore جوهرياً عن Mutex؟

الفرق الرئيسي هو الملكية. يمكن تحرير Mutex فقط من قبل الخيط المالك. يمكن تحرير Binary Semaphore من قبل أي خيط، وهو مناسب للإشارة بين الخيوط ولكنه أقل أماناً لحماية سلامة البيانات.

ما القيمة الابتدائية التي يجب اختيارها للعداد؟

القيمة الابتدائية تعتمد على السيناريو. لحماية مورد واحد — 1. لمجموعة من N اتصالاً — N. للإشارة بين الخيوط، استخدم 0 بحيث ينتظر خيط المستهلك إشارة من المنتج.

الملخص

  • Semaphore هو أداة مزامنة قائمة على العداد اقترحها دايكسترا في 1965.
  • السيمافور الثنائي يأخذ القيمتين 0 و1؛ العددي يأخذ أي قيمة غير سالبة.
  • عمليتا acquire و release ذريتان وآمنتان للخيوط.
  • على عكس الميوتكس، السيمافور غير مرتبط بخيط مالك.
  • السيمافورات العددية تُستخدم لإدارة مجموعات الموارد وتحديد التوازي.
  • النسيان release هو الخطأ الأكثر شيوعاً المؤدي لتجميد الخيوط.

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا