Semaphore یک اولیه همگامسازی است که دسترسی به منبع مشترک را از طریق شمارنده و صف نخهای منتظر کنترل میکند. بر اساس ویکیپدیا، 2024، semaphore توسط ادسخر دیکسترا در سال 1965 برای حل مسائل تعامل چندنخی پیشنهاد شد. این ابزار به شما امکان میدهد تعداد نخهایی را که همزمان با بخش بحرانی کار میکنند محدود کنید.
نکات اصلی
Semaphore — یک اولیه همگامسازی است که از شمارنده برای کنترل دسترسی به منبع مشترک استفاده میکند. این مفهوم توسط ادسخر دیکسترا در سال 1965 ارائه شد و پایهای برای تمام مکانیزمهای همگامسازی مدرن در سیستمهای عامل شد.
Semaphore یک متغیر صحیح با دو عملیات اتمی است: wait (acquire) و signal (release). عملیات wait شمارنده را کاهش میدهد و signal آن را افزایش میدهد. وقتی شمارنده به صفر میرسد، نخی که wait را فراخوانی کرده تا زمانی که signal توسط نخ دیگری اجرا شود مسدود میشود.
هدف اصلی semaphore محافظت از بخشهای بحرانی در برابر دسترسی همزمان چندین نخ است. برخلاف mutex، semaphore نیازی به وابستگی به نخ مالک ندارد، که آن را برای طیف وسیعتری از وظایف هماهنگی مناسب میکند.
مفهوم semaphore در زمینه سیستم عامل THE که در دانشگاه فناوری آیندهوون توسعه یافته بود پدید آمد. دیکسترا semaphore را به عنوان یک انتزاع ریاضی رسمی کرد و ثابت کرد که برای پیادهسازی هر اولیه همگامسازی کافی است.
مکانیزم semaphore بر دو عملیات اتمی و یک صف انتظار داخلی استوار است. هنگام فراخوانی acquire، نخ مقدار شمارنده را بررسی میکند و یا اجرا را ادامه میدهد یا تا آزاد شدن منبع مسدود میشود.
هنگام ایجاد semaphore، مقدار اولیه شمارنده مجوزها تعیین میشود. هر فراخوانی 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 semaphore قرار میدهد. نخ به حالت BLOCKED میرود و زمان پردازنده مصرف نمیکند. پس از فراخوانی release، اولین نخ در صف به حالت RUNNABLE میرود و به منبع دسترسی پیدا میکند.
در نظریه همگامسازی دو نوع اصلی semaphore وجود دارد: دودویی (binary) و شمارشی (counting). انتخاب نوع به وظیفه خاص مدیریت دسترسی به منابع بستگی دارد.
Semaphore دودویی فقط مقادیر 0 و 1 را میگیرد. از نظر رفتار شبیه mutex است، اما بدون نیاز به مالکیت — هر نخی میتواند release را اجرا کند. چنین semaphoreهایی برای پیادهسازی پرچمهای آمادگی و رویدادهای بین نخی مناسب هستند.
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("دادهها آماده")
}
Semaphore شمارشی میتواند هر مقدار غیرمنفی را بگیرد. برای مدیریت استخری از منابع همگن که چندین نمونه در دسترس دارند استفاده میشود. مثلاً استخر 5 اتصال شبکه: هر acquire یک اتصال را میگیرد، release آن را به استخر بازمیگرداند.
Semaphoreهای شمارشی برای محدود کردن سرعت دسترسی به سرویسهای خارجی و پیادهسازی استخرهای نخ ضروری هستند. آنها امکان کنترل دقیق درجه موازیسازی را بدون مدیریت دستی نخها فراهم میکنند.
| پارامتر | Semaphore دودویی | Semaphore شمارشی |
|---|---|---|
| محدوده | 0 یا 1 | از 0 تا N |
| نخهای همزمان | 1 | تا N |
| کاربرد | سیگنالدهی، پرچمها | استخر منابع، محدودسازی نرخ |
توسعهدهندگان اغلب semaphore را با mutex اشتباه میگیرند، اگرچه تفاوتهای اساسی بین آنها وجود دارد. درک این تفاوتها برای انتخاب مکانیزم همگامسازی صحیح در پروژه حیاتی است.
تفاوت کلیدی — مفهوم مالکیت. Mutex همیشه میداند کدام نخ آن را گرفته است و فقط آن نخ میتواند آن را آزاد کند. Semaphore مالک ندارد: هر نخی میتواند release را فراخوانی کند، حتی بدون فراخوانی acquire. این باعث میشود mutex برای محافظت از دادهها امنتر و semaphore برای هماهنگی انعطافپذیرتر باشد.
در عمل، mutex برای قفل متقابل ساده به دلیل بهینهسازیهای سناریوی معمولی سریعتر است. Semaphore به سربار اضافی برای نگهداری شمارنده نیاز دارد. با این حال، برای محدود کردن موازیسازی یا پیادهسازی الگوی «تولیدکننده-مصرفکننده»، semaphore ضروری است.
| ویژگی | Semaphore | Mutex |
|---|---|---|
| مالکیت | بدون مالک | دارای مالک |
| آزادسازی | هر نخی | فقط نخ مالک |
| شمارنده | از 0 تا N | دودویی |
| کاربرد | محدود کردن موازیسازی و سیگنالدهی | محافظت از بخش بحرانی |
| بازگشتپذیری | خیر | بله (reentrant) |
در توسعه برنامههای موبایل، Semaphore برای کنترل دسترسی به منابع محدود استفاده میشود: اتصالات شبکه، فایلها، پایگاههای داده و اجزای سختافزاری. پلتفرمهای مدرن پیادهسازیهای داخلی مناسبی ارائه میدهند.
یکی از موارد معمول — استخر اتصالات HTTP. برنامه میتواند همزمان بیش از 4 درخواست به سرور ارسال نکند، زیرا API ارائهدهنده موازیسازی را محدود میکند. Semaphore با مقدار اولیه 4 تضمین میکند که در هر باری تعداد درخواستهای همزمان از حد تجاوز نکند و نخهای باقیمانده در صف منتظر بمانند.
بدون semaphore، با افزایش ناگهانی فعالیت کاربر، زیرساخت سرور ممکن است دچار اضافه بار ناگهانی شود که منجر به تایماوت و خطاهای 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 با خطا خاتمه یابد، semaphore برای همیشه برای نخهای دیگر مسدود میماند. برای آزادسازی تضمینی از try/finally یا defer استفاده کنید. مشکل دوم deadlock هنگام گرفتن چندین semaphore به ترتیب متفاوت توسط نخهای مختلف است.
Semaphoreها نه تنها برای محافظت از دادهها، بلکه برای هماهنگی نخها در سناریوهای پیچیده چندنخی استفاده میشوند. آگاهی از الگوهای رایج توسعه را تسریع میکند و احتمال خطاهای همگامسازی را کاهش میدهد.
چندین الگوی اثباتشده برای استفاده از semaphoreها در پروژههای واقعی وجود دارد. دانستن آنها به جلوگیری از خطاهای معمول و ساخت سیستمهای چندنخی قابل اعتماد کمک میکند.
Semaphore با مقدار اولیه N و release دورهای از طریق تایمر محدود کردن نرخ درخواستها به API را پیادهسازی میکند. مثلاً سرویس اجازه 10 درخواست در ثانیه میدهد: semaphore با 10 شروع میشود، هر درخواست شمارنده را کاهش میدهد، و یک TimerTask جداگانه یک بار در ثانیه شمارنده را به مقدار اولیه برمیگرداند. این هم از برنامه و هم از سرور در برابر اضافه بار محافظت میکند.
در مسئله کلاسیک تولیدکننده-مصرفکننده، دو semaphore بافر را کنترل میکنند: empty (مجوز نوشتن) و full (مجوز خواندن). تولیدکننده acquire روی empty و release روی full را فراخوانی میکند، مصرفکننده — برعکس. این طرح تضمین میکند که مصرفکننده هرگز بافر خالی را نخواند و تولیدکننده آن را پر نکند.
همین طرح زیربنای بافر محدود در سیستمهای عامل — بافر حلقوی با اندازه ثابت است. در برنامههای موبایل، این الگو برای پردازش صفهای تصاویر، فایلهای ویدیویی و رویدادهای تحلیلی استفاده میشود.
Semaphoreها با موفقیت برای Throttling فراخوانیهای شبکه در سرویسهای پسزمینه استفاده میشوند. مثلاً یک برنامه تحلیلی بستههای رویداد را به سرور ارسال میکند. بدون محدود کردن نخهای همزمان، در بارهای اوج (راهاندازی برنامه، همگامسازی پس از آفلاین) تعداد درخواستهای همزمان ممکن است از محدودیتهای سرور تجاوز کند. Semaphore با مقدار اولیه 3 ارسال روان را تضمین میکند و از قفل شدن سمت سرور جلوگیری میکند.
سوالات متداول
Semaphore فقط یک شمارنده نیست، بلکه یک اولیه همگامسازی با عملیات اتمی و صف انتظار است. شمارنده معمولی نخ را مسدود نمیکند و در دسترسی رقابتی چندین نخ، اتمی بودن افزایش را تضمین نمیکند.
بله، deadlock هنگام گرفتن چندین semaphore به ترتیب متفاوت توسط نخهای مختلف امکانپذیر است. مثلاً نخ A S1 و سپس S2 را میگیرد، در حالی که نخ B — S2 و سپس S1 را. ترتیب یکسانی برای گرفتن همه semaphoreها در پروژه تعیین کنید.
نخ مسدود میشود و به حالت انتظار میرود. تا زمانی که نخ دیگری release را فراخوانی کند زمان پردازنده مصرف نمیکند. در جاوا این حالت BLOCKED است، در Swift نخ توسط GCD متوقف میشود.
تفاوت اصلی — مالکیت. Mutex فقط توسط نخ مالک آزاد میشود. Binary Semaphore میتواند توسط هر نخی آزاد شود که برای سیگنالدهی بین نخها مناسب است اما برای محافظت از یکپارچگی دادهها کمتر امن است.
مقدار اولیه به سناریو بستگی دارد. برای محافظت از یک منبع — 1. برای استخر N اتصال — N. برای سیگنالدهی بین نخها از 0 استفاده کنید تا نخ مصرفکننده منتظر سیگنال تولیدکننده بماند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید