Fungsi disebut thread-safe bila dapat dipanggil oleh beberapa thread secara bersamaan tanpa menghasilkan hasil yang salah. Subset yang lebih ketat disebut reentrant: fungsi reentrant tidak menyentuh data global sama sekali—semua state berasal dari parameter lokal dan tidak ada pemanggilan ke fungsi non-reentrant lain. Pada chapter ini kita membandingkan strtok (menyimpan pointer internal statis, tidak thread-safe) dengan strtok_r (menerima pointer state dari pemanggil, reentrant). Setelah itu, kita melihat pola alternatif: mempertahankan API non-reentrant tetapi melindunginya dengan mutex eksternal.
Masalah: State Internal Statis #
Banyak fungsi library C klasik menggunakan variabel static internal untuk menyimpan state antar pemanggilan. Desain ini lahir di era single-thread, ketika menyimpan state global dianggap wajar. Saat dua thread memanggil fungsi seperti itu secara bersamaan, state internal satu thread akan ditimpa oleh thread lain—hasil parsing menjadi kacau. Contoh paling sering disebut adalah strtok dari <string.h>.
/* File: strtok_nonreentrant.c - menunjukkan state internal strtok */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
int main(void) {
char kalimat[] = "Budi,Siti;;Andi,Joko";
/* strtok mengubah string asli - menempatkan '\0' pada delimiter */
printf("Token pertama: %s\n", strtok(kalimat, ",;"));
/* Panggilan berikutnya pakai NULL - lanjut dari posisi internal */
char *t;
while ((t = strtok(NULL, ",;")) != NULL) {
printf("Token berikutnya: %s\n", t);
}
/* Cek isi kalimat setelah diparse */
printf("Isi kalimat setelah strtok: \"");
for (size_t i = 0; i < sizeof(kalimat); i++) {
if (kalimat[i] == '\0') printf("\\0");
else printf("%c", kalimat[i]);
}
printf("\"\n");
return 0;
}Compile dan jalankan:
gcc -std=c11 -Wall -Wextra -o strtok_nonreentrant strtok_nonreentrant.c && ./strtok_nonreentrantOutput yang Dihasilkan:
Token pertama: Budi
Token berikutnya: Siti
Token berikutnya: Andi
Token berikutnya: Joko
Isi kalimat setelah strtok: "Budi\0Siti\0;Andi\0Joko\0"Mari kita telaah baris-baris kode di atas:
- Baris 7 (
char kalimat[] = "Budi,Siti;;Andi,Joko";): String mutable di stack.strtokakan memodifikasi string ini langsung—bila kita menulisconst char *kalimat, kompilator menolak karenastrtokbutuhchar *. - Baris 10 (
strtok(kalimat, ",;")): Panggilan pertama menerima pointer non-NULL.strtokmencari karakter pertama yang bukan delimiter, menulis'\0'di posisi delimiter berikutnya, dan menyimpan posisi lanjutan di variabelstaticinternal. - Baris 13 (
strtok(NULL, ",;")): ArgumenNULLmemberi sinyal “lanjutkan dari posisi internal yang tersimpan”. Inilah inti masalah thread-safety: variabelstaticinternal ini dimiliki bersama seluruh thread—tidak ada isolasi. - Baris 18-22 (loop cetak isi
kalimat): Menampilkan'\0'sebagai\0agar kita melihat bahwastrtokmengubah delimiter menjadi null terminator. Tampak";Andi"hanya satu;yang tersisa—delimiter kedua tidak diubah karenastrtokmenganggap delimiter berturut-turut sebagai satu.
Solusi Reentrant: strtok_r #
Versi reentrant dari strtok adalah strtok_r (akhiran _r berarti reentrant). Alih-alih menyimpan state di variabel statis, strtok_r meminta pemanggil menyediakan pointer saveptr yang dialokasikan di stack pemanggil. Setiap thread punya saveptr sendiri sehingga tidak ada tabrakan.
/* File: strtok_r_reentrant.c - menggunakan strtok_r yang reentrant */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
/* Fungsi yang aman dipanggil dari banyak thread karena pakai strtok_r */
static int hitung_token(char *baris, const char *delim) {
char *saveptr = NULL; /* state lokal di stack */
int hitung = 0;
char *t = strtok_r(baris, delim, &saveptr);
while (t != NULL) {
hitung++;
printf(" token: %s\n", t);
t = strtok_r(NULL, delim, &saveptr);
}
return hitung;
}
int main(void) {
char baris1[] = "apel,mangga,jeruk";
char baris2[] = "budi;siti;andi;joko";
printf("Baris 1 (koma):\n");
int n1 = hitung_token(baris1, ",");
printf(" total: %d token\n", n1);
printf("Baris 2 (titik koma):\n");
int n2 = hitung_token(baris2, ";");
printf(" total: %d token\n", n2);
return 0;
}Compile dan jalankan:
gcc -std=c11 -Wall -Wextra -o strtok_r_reentrant strtok_r_reentrant.c && ./strtok_r_reentrantOutput yang Dihasilkan:
Baris 1 (koma):
token: apel
token: mangga
token: jeruk
total: 3 token
Baris 2 (titik koma):
token: budi
token: siti
token: andi
token: joko
total: 4 tokenKarena saveptr dideklarasikan sebagai variabel lokal dalam fungsi hitung_token, dua pemanggilan fungsi tersebut—bahkan dari dua thread berbeda—memiliki saveptr terpisah di stack masing-masing. Inilah inti reentrancy: tidak ada state bersama, sehingga tidak ada race condition.
Pola Alternatif: Kunci dengan Mutex #
Terkadang kita tidak bisa mengganti fungsi non-reentrant—misalnya library eksternal yang tidak punya versi _r. Solusinya adalah membungkus pemanggilan dengan mutex, sehingga hanya satu thread pada satu waktu yang boleh masuk ke section kritis tersebut.
/* File: thread_safe_mutex.c - melindungi buffer statis dengan mutex */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <pthread.h>
#include <string.h>
static char buffer[64]; /* shared buffer */
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
/* Akses aman ke buffer statis lewat mutex */
static void tulis_buffer(const char *pesan) {
pthread_mutex_lock(&mtx); /* masuk critical section */
strncpy(buffer, pesan, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
printf("[thread %lu] buffer = %s\n",
(unsigned long) pthread_self(), buffer);
pthread_mutex_unlock(&mtx); /* keluar critical section */
}
static void *pekerja(void *arg) {
const char *nama = (const char *) arg;
tulis_buffer(nama);
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, pekerja, "Budi");
pthread_create(&t2, NULL, pekerja, "Siti");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Buffer akhir: %s\n", buffer);
return 0;
}Compile dan jalankan:
gcc -std=c11 -Wall -Wextra -pthread -o thread_safe_mutex thread_safe_mutex.c && ./thread_safe_mutexOutput yang Dihasilkan (urutan thread bisa terbalik karena penjadwalan):
[thread 140647844021952] buffer = Siti
[thread 140647852414656] buffer = Budi
Buffer akhir: BudiMari kita telaah baris-baris kode di atas:
- Baris 9 (
static char buffer[64];): Buffer statis global—secara desain tidak thread-safe. Dua thread yang menulis tanpa koordinasi akan menghasilkan buffer berisi campuran karakter dari keduanya. - Baris 10 (
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;): Inisialisasi statis mutex dengan macroPTHREAD_MUTEX_INITIALIZER. Cara ini menghindari pemanggilanpthread_mutex_initeksplisit dimain. Macro ini bekerja untuk mutex dengan atribut default. - Baris 13 (
pthread_mutex_lock(&mtx);): Thread yang memanggil fungsi ini akan meminta kunci. Bila kunci sedang dipegang thread lain, thread ini memblokir hingga kunci dilepas. - Baris 14-17 (operasi pada
buffer): Operasi padabufferkini berada dalam critical section. Tidak ada thread lain yang bisa masuk selamapthread_mutex_unlockbelum dipanggil. - Baris 18 (
pthread_mutex_unlock(&mtx);): Melepaskan kunci. Mutex di sini bertindak sebagai penjaga gerbang: pada satu waktu hanya satu thread yang boleh menyentuhbuffer.
Daftar Cepat Fungsi Non-Reentrant dan Penggantinya #
POSIX memberi daftar tegas fungsi non-reentrant beserta versi reentrant-nya. Tabel berikut adalah yang paling sering ditemui di kode sehari-hari:
| Non-Reentrant | Reentrant | Catatan |
|---|---|---|
strtok | strtok_r | Tokenisasi string dengan delimiter |
asctime | asctime_r | Konversi struct tm ke string |
ctime | ctime_r | Konversi time_t ke string |
gmtime | gmtime_r | Konversi time_t ke UTC struct tm |
localtime | localtime_r | Konversi time_t ke lokal struct tm |
rand | (gunakan rand_r atau drand48_r) | Generator angka acak |
gethostbyname | gethostbyname_r | Resolusi DNS (lebih baik gunakan getaddrinfo) |