Apa itu read-write lock?

Read-write lock (rwlock) membedakan dua jenis akses: baca dan tulis. Banyak thread boleh membaca data bersamaan, karena baca tidak mengubah data dan aman dilakukan paralel. Tapi hanya satu thread yang boleh menulis, dan saat ada yang menulis, tidak ada thread lain — pembaca maupun penulis lain — yang boleh mengakses data.

Mutex biasa memperlakukan semua akses, baca maupun tulis, sama-sama eksklusif. Rwlock tidak.

Ilustrasi

Mutex biasa (semua akses serial, walau cuma baca):
Reader1: LOCK → baca → UNLOCK
Reader2:        (nunggu)      LOCK → baca → UNLOCK
Writer1:        (nunggu)             (nunggu)      LOCK → tulis → UNLOCK
 
Read-write lock (reader boleh paralel):
Reader1: RDLOCK → baca → UNLOCK
Reader2: RDLOCK → baca →         UNLOCK
Writer1:          (nunggu)               WRLOCK → tulis → UNLOCK

Reader1 dan Reader2 baca bersamaan. Writer1 menunggu sampai keduanya selesai, dan selama Writer1 menulis, tidak ada reader baru yang boleh masuk.

Kenapa diperlukan

Bayangkan sebuah wallet yang lebih sering dibaca (cek saldo) daripada ditulis (update saldo) — rasio yang umum di aplikasi nyata, misalnya 100 pembacaan untuk setiap 1 penulisan. Kalau semua akses dilindungi mutex biasa, semua pembacaan tetap serial satu-satu, padahal sebenarnya aman dilakukan bersamaan karena tidak ada yang mengubah data.

Rwlock memanfaatkan fakta ini: baca-baca tidak saling mengganggu, jadi tidak perlu diserialkan. Hanya operasi yang melibatkan tulis yang benar-benar butuh eksklusivitas.

Implementasi

Struktur proyek

.
├── main.c
├── wallet.c
└── wallet.h

Step 1: interface wallet

#ifndef WALLET_H
#define WALLET_H
 
void wallet_init(void);
int lihat_saldo(int thread_id);
void update_saldo(int nominal, int thread_id);
 
#endif

Step 2: wallet dengan rwlock

lihat_saldo pakai rdlock (boleh paralel), update_saldo pakai wrlock (eksklusif).

#include "wallet.h"
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
 
static int saldo = 0;
static pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
 
void wallet_init(void)
{
    saldo = 0;
}
 
int lihat_saldo(int thread_id)
{
    pthread_rwlock_rdlock(&rwlock);
 
    printf("[Reader %d] membaca saldo: %d\n", thread_id, saldo);
    usleep(200000);
    int hasil = saldo;
 
    pthread_rwlock_unlock(&rwlock);
    return hasil;
}
 
void update_saldo(int nominal, int thread_id)
{
    pthread_rwlock_wrlock(&rwlock);
 
    printf("[Writer %d] menulis saldo: %d -> %d\n", thread_id, saldo, nominal);
    usleep(200000);
    saldo = nominal;
 
    pthread_rwlock_unlock(&rwlock);
}

Baik rdlock maupun wrlock dilepas dengan fungsi yang sama, pthread_rwlock_unlock — tidak ada rdunlock/wrunlock terpisah.

Step 3: reader dan writer

#include "wallet.h"
#include <pthread.h>
#include <stdio.h>
 
void *reader(void *arg)
{
    int id = *(int *)arg;
    lihat_saldo(id);
    return NULL;
}
 
void *writer(void *arg)
{
    int id = *(int *)arg;
    update_saldo(1000, id);
    return NULL;
}
 
int main(void)
{
    wallet_init();
 
    pthread_t readers[4], writer_thread;
    int reader_ids[4] = {1, 2, 3, 4};
    int writer_id = 1;
 
    for (int i = 0; i < 4; i++) {
        pthread_create(&readers[i], NULL, reader, &reader_ids[i]);
    }
    pthread_create(&writer_thread, NULL, writer, &writer_id);
 
    for (int i = 0; i < 4; i++) {
        pthread_join(readers[i], NULL);
    }
    pthread_join(writer_thread, NULL);
 
    return 0;
}

Penjelasan

pthread_rwlock_rdlock mengizinkan thread lain yang juga memanggil rdlock untuk masuk bersamaan, tapi akan diblok kalau ada thread lain yang sedang memegang wrlock.

pthread_rwlock_wrlock bersifat eksklusif — tidak ada reader maupun writer lain yang boleh masuk selama lock ini dipegang, dan dia sendiri akan diblok kalau ada reader atau writer lain yang sedang aktif.

pthread_rwlock_unlock dipakai untuk melepas keduanya. Kalau yang dilepas adalah rdlock terakhir yang aktif, writer yang menunggu baru bisa masuk.

Timeline eksekusi

Time Reader1 Reader2 Writer1 Saldo
t0 RDLOCK RDLOCK (nunggu) 0
t1 baca baca (nunggu) 0
t2 UNLOCK UNLOCK (nunggu) 0
t3 WRLOCK 0
t4 tulis: 0 → 1000 1000
t5 UNLOCK 1000

Reader1 dan Reader2 jalan bersamaan di t0-t2. Writer1 baru masuk setelah keduanya selesai.

Kompilasi

gcc -o wallet main.c wallet.c -lpthread
./wallet

Best practices

Pakai rwlock kalau read memang jauh lebih sering dari write — kalau rasionya mendekati 1:1, overhead rwlock bisa membuatnya lebih lambat dari mutex biasa. Manfaatnya juga baru terasa kalau critical section-nya cukup besar, karena rwlock lebih kompleks secara internal dibanding mutex.

Waspadai writer starvation: implementasi default bisa membiarkan writer menunggu lama kalau reader terus berdatangan. Di Linux bisa diatur lewat pthread_rwlockattr_setkind_np. Rwlock juga tidak reentrant untuk writer — thread yang sudah pegang wrlock lalu coba wrlock lagi akan deadlock. Destroy dengan pthread_rwlock_destroy setelah selesai.

Kesimpulan

Rwlock cocok dipakai kalau akses baca jauh lebih sering daripada tulis, dan critical section-nya cukup besar untuk membuat paralelisme reader terasa manfaatnya. Rdlock untuk boleh rame-rame asal cuma baca, wrlock untuk harus sendirian.

Footnotes