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>.

c
/* 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:

bash
gcc -std=c11 -Wall -Wextra -o strtok_nonreentrant strtok_nonreentrant.c && ./strtok_nonreentrant

Output yang Dihasilkan:

text
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:

  1. Baris 7 (char kalimat[] = "Budi,Siti;;Andi,Joko";): String mutable di stack. strtok akan memodifikasi string ini langsung—bila kita menulis const char *kalimat, kompilator menolak karena strtok butuh char *.
  2. Baris 10 (strtok(kalimat, ",;")): Panggilan pertama menerima pointer non-NULL. strtok mencari karakter pertama yang bukan delimiter, menulis '\0' di posisi delimiter berikutnya, dan menyimpan posisi lanjutan di variabel static internal.
  3. Baris 13 (strtok(NULL, ",;")): Argumen NULL memberi sinyal “lanjutkan dari posisi internal yang tersimpan”. Inilah inti masalah thread-safety: variabel static internal ini dimiliki bersama seluruh thread—tidak ada isolasi.
  4. Baris 18-22 (loop cetak isi kalimat): Menampilkan '\0' sebagai \0 agar kita melihat bahwa strtok mengubah delimiter menjadi null terminator. Tampak ";Andi" hanya satu ; yang tersisa—delimiter kedua tidak diubah karena strtok menganggap 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.

c
/* 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:

bash
gcc -std=c11 -Wall -Wextra -o strtok_r_reentrant strtok_r_reentrant.c && ./strtok_r_reentrant

Output yang Dihasilkan:

text
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 token

Karena 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.

c
/* 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:

bash
gcc -std=c11 -Wall -Wextra -pthread -o thread_safe_mutex thread_safe_mutex.c && ./thread_safe_mutex

Output yang Dihasilkan (urutan thread bisa terbalik karena penjadwalan):

text
[thread 140647844021952] buffer = Siti
[thread 140647852414656] buffer = Budi
Buffer akhir: Budi

Mari kita telaah baris-baris kode di atas:

  1. 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.
  2. Baris 10 (static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;): Inisialisasi statis mutex dengan macro PTHREAD_MUTEX_INITIALIZER. Cara ini menghindari pemanggilan pthread_mutex_init eksplisit di main. Macro ini bekerja untuk mutex dengan atribut default.
  3. 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.
  4. Baris 14-17 (operasi pada buffer): Operasi pada buffer kini berada dalam critical section. Tidak ada thread lain yang bisa masuk selama pthread_mutex_unlock belum dipanggil.
  5. Baris 18 (pthread_mutex_unlock(&mtx);): Melepaskan kunci. Mutex di sini bertindak sebagai penjaga gerbang: pada satu waktu hanya satu thread yang boleh menyentuh buffer.

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-ReentrantReentrantCatatan
strtokstrtok_rTokenisasi string dengan delimiter
asctimeasctime_rKonversi struct tm ke string
ctimectime_rKonversi time_t ke string
gmtimegmtime_rKonversi time_t ke UTC struct tm
localtimelocaltime_rKonversi time_t ke lokal struct tm
rand(gunakan rand_r atau drand48_r)Generator angka acak
gethostbynamegethostbyname_rResolusi DNS (lebih baik gunakan getaddrinfo)