Apa itu semaphore?
Semaphore adalah mekanisme sinkronisasi yang pakai counter untuk mengontrol berapa banyak thread yang boleh mengakses suatu resource secara bersamaan. Mutex hanya mengizinkan satu thread masuk critical section. Semaphore bisa mengizinkan N thread sekaligus, di mana N ditentukan saat inisialisasi.
Ada dua jenisnya: counting semaphore, di mana counter bisa lebih dari 1 (misal membatasi maksimal 3 koneksi database aktif), dan binary semaphore, yang counternya cuma 0 atau 1 dan mirip mutex1 — walau secara semantik keduanya tetap beda (lihat bagian caveat di bawah).
Ilustrasi
Mutex (kapasitas 1):
Thread1: LOCK → kerja → UNLOCK
Thread2: (nunggu) LOCK → kerja → UNLOCK
Thread3: (nunggu) (nunggu) LOCK → kerja → UNLOCK
Semaphore (kapasitas 2):
Thread1: WAIT → kerja → POST
Thread2: WAIT → kerja → POST
Thread3: (nunggu) WAIT → kerja → POST
Thread1 dan Thread2 jalan bersamaan karena kapasitasnya 2. Thread3 baru masuk setelah salah satu dari mereka selesai.
Kenapa diperlukan
Mutex cocok kalau akses harus benar-benar eksklusif. Tapi kadang kita justru ingin beberapa thread mengakses resource bersamaan, cuma dibatasi jumlahnya. Contohnya connection pool database dengan 3 slot koneksi, sementara ada 10 thread yang butuh query bersamaan — thread 1-3 dapat slot dan mulai query, sisanya menunggu, dan begitu satu slot kosong, thread berikutnya yang menunggu langsung masuk.
Kalau ini dipaksa pakai mutex (kapasitas 1), cuma satu query yang jalan di satu waktu, padahal sebenarnya boleh 3 sekaligus. Semaphore menjaga supaya jumlah thread aktif tidak melebihi N, tanpa membuang kapasitas yang sebenarnya tersedia.
Implementasi
Struktur proyek
.
├── main.c
├── db_pool.c
└── db_pool.h
Step 1: interface connection pool
#ifndef DB_POOL_H
#define DB_POOL_H
void init_pool(void);
void query_db(int thread_id);
void destroy_pool(void);
#endif
Step 2: pool dengan semaphore
#include "db_pool.h"
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>
#define MAX_KONEKSI 3
sem_t sem_pool;
void init_pool(void)
{
// 0 = shared antar thread, 3 = jumlah slot koneksi
sem_init(&sem_pool, 0, MAX_KONEKSI);
}
void query_db(int thread_id)
{
printf("Thread %d menunggu slot koneksi...\n", thread_id);
sem_wait(&sem_pool);
printf("Thread %d dapat slot, menjalankan query...\n", thread_id);
usleep(500000);
printf("Thread %d selesai, melepas slot.\n", thread_id);
sem_post(&sem_pool);
}
void destroy_pool(void)
{
sem_destroy(&sem_pool);
}
usleep() di sini cuma mensimulasikan waktu query, supaya thread lain punya kesempatan nyata untuk berebut slot.
Step 3: 10 thread, 3 slot
#include "db_pool.h"
#include <pthread.h>
#include <stdio.h>
#define JUMLAH_THREAD 10
void *worker(void *arg)
{
int id = *(int *)arg;
query_db(id);
return NULL;
}
int main(void)
{
init_pool();
pthread_t threads[JUMLAH_THREAD];
int ids[JUMLAH_THREAD];
printf("Memulai %d thread dengan 3 slot koneksi...\n\n", JUMLAH_THREAD);
for (int i = 0; i < JUMLAH_THREAD; i++) {
ids[i] = i + 1;
pthread_create(&threads[i], NULL, worker, &ids[i]);
}
for (int i = 0; i < JUMLAH_THREAD; i++) {
pthread_join(threads[i], NULL);
}
destroy_pool();
printf("\nSemua thread selesai.\n");
return 0;
}
Penjelasan
sem_init(&sem_pool, 0, MAX_KONEKSI) — parameter kedua (pshared) bernilai 0 karena semaphore ini dipakai antar thread dalam satu proses, bukan antar proses lewat shared memory. Parameter ketiga adalah nilai awal counter, sama dengan jumlah slot yang tersedia.
sem_wait() mengurangi counter sebanyak 1. Kalau counter sudah 0, thread yang memanggilnya terblokir sampai ada thread lain memanggil sem_post(). Anggap saja mengambil satu tiket dari tiket yang tersedia.
Bagian antara sem_wait() dan sem_post() boleh dimasuki lebih dari satu thread sekaligus, sebanyak nilai counter awal — beda dari critical section mutex yang selalu eksklusif untuk satu thread.
sem_post() menambah counter sebanyak 1 dan membangunkan salah satu thread yang menunggu, kalau ada. Ini kebalikan dari mengambil tiket tadi.
sem_destroy() membersihkan resource semaphore, dipanggil setelah semua thread selesai.
Timeline eksekusi
Semaphore kapasitas 3, untuk 5 thread pertama:
| Time | Thread1 | Thread2 | Thread3 | Thread4 | Thread5 | Counter |
|---|---|---|---|---|---|---|
| t0 | start | start | start | start | start | 3 |
| t1 | WAIT | WAIT | WAIT | (nunggu) | (nunggu) | 0 |
| t2 | query | query | query | (nunggu) | (nunggu) | 0 |
| t3 | POST | WAIT | (nunggu) | 1 → 0 | ||
| t4 | selesai | query | query | query | (nunggu) | 0 |
| t5 | POST | WAIT | 1 → 0 | |||
| t6 | selesai | query | query | query | 0 |
Di t1, tiga thread langsung dapat slot bersamaan — sesuatu yang tidak mungkin terjadi dengan mutex.
Kompilasi
gcc -o db_pool main.c db_pool.c -lpthread
./db_pool
Output (urutan antar thread bisa berbeda-beda):
Memulai 10 thread dengan 3 slot koneksi...
Thread 1 menunggu slot koneksi...
Thread 2 menunggu slot koneksi...
Thread 3 menunggu slot koneksi...
Thread 1 dapat slot, menjalankan query...
Thread 2 dapat slot, menjalankan query...
Thread 3 dapat slot, menjalankan query...
Thread 4 menunggu slot koneksi...
...
Semua thread selesai.
Best practices
Set kapasitas sesuai batas resource yang sebenarnya, misalnya max_connections di config database, bukan angka sembarang. Pasangkan selalu sem_wait dengan sem_post, sama pentingnya dengan lock-unlock di mutex. Hati-hati jangan sampai sem_post dipanggil lebih banyak dari seharusnya — counter bisa melebihi kapasitas asli kalau logikanya salah, dan semaphore tidak melindungi ini secara otomatis. Destroy semaphore setelah semua thread selesai, dan cek return value dari sem_init, sem_wait, sem_post untuk error handling.
Kesimpulan
Mutex membatasi akses jadi satu thread. Semaphore membatasi akses jadi maksimal N thread, dan itu bedanya. Kalau resource kamu memang punya kapasitas lebih dari 1 — connection pool, rate limiting, dan sejenisnya — semaphore memanfaatkan kapasitas itu secara maksimal, alih-alih memaksa semuanya serial seperti mutex.
Footnotes
-
Mutex sebenarnya bisa dianggap kasus khusus dari semaphore dengan kapasitas 1 (binary semaphore), tapi soal ownership keduanya tetap beda: mutex idealnya di-unlock oleh thread yang sama yang lock, semaphore tidak punya batasan itu. ↩