Semaphore adalah primitif sinkronisasi yang mengelola akses ke sumber daya bersama melalui penghitung dan antrian thread yang menunggu. Menurut Wikipedia, 2024, semaphore diusulkan oleh Edsger Dijkstra pada tahun 1965 untuk menyelesaikan masalah interaksi multithread. Alat ini memungkinkan membatasi jumlah thread yang bekerja secara bersamaan dengan seksi kritis.
Poin Utama
Semaphore — adalah primitif sinkronisasi yang menggunakan penghitung untuk mengelola akses ke sumber daya bersama. Konsep ini diusulkan oleh Edsger Dijkstra pada tahun 1965 dan menjadi fondasi semua mekanisme sinkronisasi modern dalam sistem operasi.
Semaphore adalah variabel bilangan bulat dengan dua operasi atomik: wait (acquire) dan signal (release). Operasi wait mengurangi penghitung, dan signal meningkatkannya. Ketika penghitung mencapai nol, thread yang memanggil wait akan diblokir sampai signal dijalankan oleh thread lain.
Tujuan utama semaphore adalah melindungi seksi kritis dari akses simultan oleh beberapa thread. Berbeda dengan mutex, semaphore tidak memerlukan keterikatan pada thread pemilik, yang membuatnya cocok untuk berbagai tugas koordinasi yang lebih luas.
Konsep semaphore muncul dalam konteks sistem operasi THE, yang dikembangkan di Technische Hogeschool Eindhoven. Dijkstra memformalkan semaphore sebagai abstraksi matematika, membuktikan kecukupannya untuk mengimplementasikan primitif sinkronisasi apa pun.
Mekanisme semaphore didasarkan pada dua operasi atomik dan antrian tunggu internal. Saat memanggil acquire, thread memeriksa nilai penghitung dan melanjutkan eksekusi atau diblokir sampai sumber daya dibebaskan.
Saat membuat semaphore, nilai awal penghitung izin ditetapkan. Setiap panggilan acquire mengurangi penghitung sebesar 1. Jika setelah itu penghitung menjadi negatif, thread akan diblokir. Operasi release meningkatkan penghitung dan membangunkan salah satu thread yang menunggu.
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} bekerja")
} finally {
semaphore.release()
}
}
Ketika thread memanggil acquire dengan penghitung nol, OS menempatkannya dalam antrian FIFO semaphore. Thread beralih ke status BLOCKED, tidak menghabiskan waktu prosesor. Setelah panggilan release, thread pertama dalam antrian beralih ke status RUNNABLE dan mendapatkan akses ke sumber daya.
Dalam teori sinkronisasi, dua jenis utama semaphore dibedakan: biner (binary) dan penghitung (counting). Pemilihan jenis tergantung pada tugas spesifik manajemen akses ke sumber daya.
Semaphore biner hanya mengambil nilai 0 dan 1. Perilakunya menyerupai mutex, tetapi tanpa persyaratan kepemilikan — thread mana pun dapat mengeksekusi release. Semaphore semacam itu berguna untuk mengimplementasikan bendera kesiapan dan peristiwa antar thread.
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("Data siap")
}
Semaphore penghitung dapat mengambil nilai non-negatif apa pun. Digunakan untuk mengelola kumpulan sumber daya homogen di mana beberapa instance tersedia. Misalnya, kumpulan 5 koneksi jaringan: setiap acquire mengambil satu koneksi, release mengembalikannya ke kumpulan.
Semaphore penghitung sangat diperlukan untuk membatasi kecepatan akses ke layanan eksternal dan mengimplementasikan kumpulan thread. Mereka memungkinkan kontrol yang tepat atas tingkat paralelisme tanpa manajemen thread manual.
| Parameter | Semaphore Biner | Semaphore Penghitung |
|---|---|---|
| Rentang | 0 atau 1 | dari 0 hingga N |
| Thread simultan | 1 | hingga N |
| Penerapan | sinyalisasi, bendera | kumpulan sumber daya, rate limiting |
Pengembang sering bingung antara semaphore dan mutex, meskipun ada perbedaan mendasar di antara keduanya. Memahami perbedaan ini sangat penting untuk memilih mekanisme sinkronisasi yang tepat dalam proyek.
Perbedaan utama — konsep kepemilikan. Mutex selalu tahu thread mana yang mengambilnya, dan hanya thread itu yang dapat melepaskannya. Semaphore tidak memiliki pemilik: thread mana pun dapat memanggil release, bahkan tanpa memanggil acquire. Ini membuat mutex lebih aman untuk perlindungan data, dan semaphore lebih fleksibel untuk koordinasi.
Dalam praktiknya, mutex lebih cepat untuk penguncian timbal balik sederhana berkat optimalisasi untuk skenario tipikal. Semaphore memerlukan overhead tambahan untuk memelihara penghitung. Namun, untuk membatasi paralelisme atau mengimplementasikan pola “produsen-konsumen”, semaphore sangat diperlukan.
| Karakteristik | Semaphore | Mutex |
|---|---|---|
| Kepemilikan | tanpa pemilik | memiliki pemilik |
| Pelepasan | thread mana pun | hanya thread pemilik |
| Penghitung | dari 0 hingga N | biner |
| Use case | membatasi paralelisme dan sinyalisasi | perlindungan seksi kritis |
| Rekursi | tidak | ya (reentrant) |
Dalam pengembangan aplikasi mobile, Semaphore digunakan untuk mengelola akses ke sumber daya terbatas: koneksi jaringan, file, database, dan komponen perangkat keras. Platform modern menyediakan implementasi bawaan yang nyaman.
Salah satu kasus tipikal — kumpulan koneksi HTTP. Aplikasi dapat mengirim tidak lebih dari 4 permintaan secara bersamaan ke server, karena API penyedia membatasi paralelisme. Semaphore dengan nilai awal 4 menjamin bahwa pada beban apa pun jumlah permintaan simultan tidak akan melebihi batas, dan thread lainnya akan menunggu dalam antrian.
Tanpa semaphore, saat peningkatan aktivitas pengguna yang tiba-tiba, infrastruktur server dapat mengalami kelebihan beban mendadak, yang menyebabkan timeout dan kesalahan 429 Too Many Requests. Semaphore berfungsi seperti sekring, hanya melewatkan sejumlah panggilan simultan yang ditentukan secara ketat terlepas dari jumlah thread aktif.
Android menyediakan kelas Semaphore dari paket java.util.concurrent. Mari kita lihat contoh membatasi permintaan jaringan simultan hingga dua thread untuk mencegah kelebihan beban server.
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
Di iOS, DispatchSemaphore dari GCD menyelesaikan tugas yang sama. Pengembang menggunakannya untuk menyinkronkan akses ke sumber daya dalam kode asinkron tanpa memblokir thread utama.
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
Kesalahan paling umum — release yang terlupakan saat terjadi pengecualian. Jika thread berakhir dengan kesalahan sebelum memanggil release, semaphore akan tetap diblokir selamanya untuk thread lainnya. Gunakan try/finally atau defer untuk pelepasan yang terjamin. Masalah kedua adalah deadlock saat mengambil beberapa semaphore dalam urutan berbeda oleh thread yang berbeda.
Semaphore tidak hanya digunakan untuk perlindungan data, tetapi juga untuk koordinasi thread dalam skenario multithread yang kompleks. Pengetahuan tentang pola umum mempercepat pengembangan dan mengurangi kemungkinan kesalahan sinkronisasi.
Ada beberapa pola yang terbukti dalam penerapan semaphore di proyek nyata. Mengetahuinya membantu menghindari kesalahan tipikal dan membangun sistem multithread yang andal.
Semaphore dengan nilai awal N dan pelepasan periodik melalui timer mengimplementasikan pembatasan kecepatan permintaan ke API. Misalnya, layanan mengizinkan 10 permintaan per detik: semaphore mulai dengan 10, setiap permintaan mengurangi penghitung, dan TimerTask terpisah mengembalikan penghitung ke nilai awal sekali per detik. Ini melindungi aplikasi dan server dari kelebihan beban.
Dalam tugas klasik produsen-konsumen, dua semaphore mengelola buffer: empty (izin menulis) dan full (izin membaca). Produsen memanggil acquire pada empty dan release pada full, Konsumen — sebaliknya. Skema ini menjamin bahwa Konsumen tidak akan pernah membaca buffer kosong, dan Produsen tidak akan mengisinya berlebihan.
Skema yang sama mendasari buffer terbatas dalam sistem operasi — buffer melingkar dengan ukuran tetap. Dalam aplikasi mobile, pola ini digunakan untuk memproses antrian gambar, file video, dan peristiwa analitik.
Semaphore berhasil digunakan untuk throttling panggilan jaringan di layanan latar belakang. Misalnya, aplikasi analitik mengirim paket peristiwa ke server. Tanpa membatasi thread konkuren, pada beban puncak (memulai aplikasi, sinkronisasi setelah offline) jumlah permintaan simultan dapat melebihi batas server. Semaphore dengan nilai awal 3 menjamin pengiriman yang lancar dan mencegah pemblokiran di sisi server.
Pertanyaan yang Sering Diajukan
Semaphore bukan sekadar penghitung, tetapi primitif sinkronisasi dengan operasi atomik dan antrian tunggu. Penghitung biasa tidak memblokir thread dan tidak menjamin atomisitas incrementasi saat akses konkuren oleh beberapa thread.
Ya, deadlock mungkin terjadi saat mengambil beberapa semaphore dalam urutan berbeda oleh thread yang berbeda. Misalnya, thread A mengambil S1, lalu S2, dan thread B — S2, lalu S1. Tetapkan urutan pengambilan yang seragam untuk semua semaphore dalam proyek.
Thread diblokir dan beralih ke status tunggu. Thread tidak menghabiskan waktu prosesor sampai thread lain memanggil release. Di Java ini adalah status BLOCKED, di Swift thread dihentikan oleh GCD.
Perbedaan utama — kepemilikan. Mutex hanya dapat dilepaskan oleh thread pemilik. Binary Semaphore dapat dilepaskan oleh thread mana pun, yang nyaman untuk sinyalisasi antar thread, tetapi kurang aman untuk melindungi integritas data.
Nilai awal tergantung pada skenario. Untuk melindungi satu sumber daya — 1. Untuk kumpulan N koneksi — N. Untuk sinyalisasi antar thread gunakan 0, sehingga thread konsumen menunggu sinyal dari produsen.
Ringkasan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga