Concurrency dan parallelism sering dipakai bergantian, padahal keduanya berbeda. Concurrency adalah desain program yang menangani banyak tugas dalam progres—tidak harus dieksekusi pada saat yang sama. Parallelism adalah eksekusi banyak tugas secara harfiah bersamaan, pada inti CPU berbeda. Sebuah program concurrent bisa berjalan pada satu CPU pun (scheduler beralih cepat antar tugas), sementara program parallel butuh hardware multicore. Pada chapter ini kita menulis dua demo: pertama memperlihatkan concurrency lewat dua thread yang menunggu sleep(1) selesai dalam total 1 detik (bukan 2 detik), kedua membandingkan sequential vs parallel menghitung bilangan prima untuk melihat speedup aktual.

Concurrency: Banyak Tugas dalam Progres #

Concurrency bukan tentang eksekusi serempak, melainkan struktur program: tugas-tugas dipecah menjadi unit yang dapat dijadwalkan. Sistem operasi mengatur penjadwalan preemptive—thread bisa dihentikan sementara di tengah instruksi dan dilanjutkan nanti. Karena itulah dua tugas yang masing-masing tidur 1 detik dapat diselesaikan dalam total ~1 detik, bukan 2 detik, ketika dijalankan secara concurrent.

c
/* File: concurrent_sleep.c - dua tugas sleep(1) selesai dalam ~1 detik */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>

static double sekarang_detik(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec + ts.tv_nsec / 1e9;
}

static void *tugas(void *arg) {
    long id = (long) arg;
    sleep(1);                       /* simulasi kerja I/O 1 detik */
    printf("tugas %ld selesai\n", id);
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    double mulai = sekarang_detik();

    pthread_create(&t1, NULL, tugas, (void *) 1L);
    pthread_create(&t2, NULL, tugas, (void *) 2L);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    double selesai = sekarang_detik();
    printf("Total waktu (concurrent): %.2f detik\n", selesai - mulai);
    return 0;
}

Compile dan jalankan:

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

Output yang Dihasilkan:

text
tugas 1 selesai
tugas 2 selesai
Total waktu (concurrent): 1.00 detik

Mari kita telaah baris-baris kode di atas:

  1. Baris 8-11 (sekarang_detik): Mengukur waktu dengan clock_gettime(CLOCK_MONOTONIC, ...). CLOCK_MONOTONIC dipilih karena tidak terpengaruh perubahan jam dinding (NTP)—ideal untuk mengukur durasi.
  2. Baris 13-18 (tugas): Setiap thread tidur 1 detik lalu mencetak pesan. Tanpa concurrency, dua tugas ini berurutan butuh 2 detik; dengan thread, keduanya dijadwalkan serentak.
  3. Baris 21-22 (pthread_create(...)): Membuat dua thread hampir bersamaan. Pada sistem single-core, scheduler kernel akan mengalihkan CPU antara t1 dan t2 saat keduanya sleep, sehingga total tetap ~1 detik.
  4. Baris 24-25 (pthread_join(...)): main memblokir hingga kedua thread selesai. Karena keduanya selesai hampir bersamaan, total waktu ≈ durasi tugas terpanjang (1 detik), bukan jumlahnya.

Parallelism: Eksekusi Bersamaan pada Core Berbeda #

Parallelism memerlukan hardware multicore. Bila mesin punya 2 core atau lebih, dua thread benar-benar dijalankan serentak—bukan lewat context switch. Untuk membuktikan hal ini, kita membandingkan komputasi murni CPU (menghitung bilangan prima) antara mode sequential dan mode parallel.

c
/* File: parallel_prima.c - sequential vs parallel menghitung prima */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <pthread.h>
#include <time.h>

static double sekarang_detik(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec + ts.tv_nsec / 1e9;
}

/* hitung jumlah bilangan prima dalam range [start, end) */
static long jumlah_prima(long start, long end) {
    long total = 0;
    for (long n = start; n < end; n++) {
        int prima = (n >= 2);
        for (long d = 2; d * d <= n; d++) {
            if (n % d == 0) { prima = 0; break; }
        }
        if (prima) total++;
    }
    return total;
}

typedef struct { long start; long end; long hasil; } Range;

static void *kerja(void *arg) {
    Range *r = (Range *) arg;
    r->hasil = jumlah_prima(r->start, r->end);
    return NULL;
}

int main(void) {
    const long N = 500000;
    Range r1 = { 2,    N / 2, 0 };
    Range r2 = { N / 2, N,    0 };

    /* Sequential: satu thread kerjakan dua range */
    double t0 = sekarang_detik();
    long total_seq = jumlah_prima(r1.start, r1.end) + jumlah_prima(r2.start, r2.end);
    double t1 = sekarang_detik();

    /* Parallel: dua thread kerjakan range masing-masing */
    double t2 = sekarang_detik();
    pthread_t a, b;
    pthread_create(&a, NULL, kerja, &r1);
    pthread_create(&b, NULL, kerja, &r2);
    pthread_join(a, NULL);
    pthread_join(b, NULL);
    long total_par = r1.hasil + r2.hasil;
    double t3 = sekarang_detik();

    printf("Sequential: %ld prima dalam %.3f detik\n", total_seq, t1 - t0);
    printf("Parallel  : %ld prima dalam %.3f detik\n", total_par, t3 - t2);
    printf("Speedup   : %.2fx\n", (t1 - t0) / (t3 - t2));
    return 0;
}

Compile dengan optimasi -O2 agar compiler tidak membuang waktu pada branch yang predictable:

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

Output yang Dihasilkan (hasil bergantung pada jumlah core mesin):

text
Sequential: 41538 prima dalam 0.074 detik
Parallel  : 41538 prima dalam 0.046 detik
Speedup   : 1.62x

Total prima sequential dan parallel identik (41538), membuktikan kedua mode menghasilkan jawaban yang sama—perbedaannya hanya waktu. Speedup 1.62x bukan 2x karena ada overhead pembuatan thread, sinkronisasi pthread_join, dan kernel scheduler. Pada mesin 2-core, speedup teoretis maksimum adalah 2x; pada mesin N-core, hukum Amdahl menyatakan speedup dibatasi oleh bagian serial program.

Tantangan Koordinasi: Shared State dan Sinkronisasi #

Concurrency menambah kompleksitas baru: bila dua thread membaca dan menulis variabel yang sama tanpa koordinasi, hasil akhirnya tidak terduga. Kita sebut situasi ini race condition. Tiga mekanisme standar POSIX untuk mengatasi masalah ini:

  • Mutex (pthread_mutex_t): kunci eksklusif. Hanya satu thread yang dapat memegang kunci pada satu waktu.
  • Condition Variable (pthread_cond_t): sinyal “kondisi sudah terpenuhi” untuk membangunkan thread yang menunggu.
  • Atomics (_Atomic): operasi yang tidak dapat diinterupsi oleh thread lain, berguna untuk counter sederhana.
c
/* File: tantangan_koordinasi.c - ilustrasi masalah tanpa sinkronisasi */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <pthread.h>

static long saldo = 0;             /* shared state tanpa proteksi */

static void *tambah_saldo(void *arg) {
    long jumlah = (long) arg;
    for (long i = 0; i < 100000; i++) {
        saldo += jumlah;           /* BACA-TAMBAH-TULIS: race condition! */
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, tambah_saldo, (void *) 1L);
    pthread_create(&t2, NULL, tambah_saldo, (void *) 1L);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("Saldo akhir (idealnya 200000): %ld\n", saldo);
    return 0;
}

Compile tanpa -O2 agar compiler tidak mengoptimalkan loop menjadi satu penambahan—kita ingin compiler mempertahankan instruksi load/store per iterasi:

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

Output yang Dihasilkan (nilai berubah-ubah antar eksekusi):

text
Saldo akhir (idealnya 200000): 137914
Saldo akhir (idealnya 200000): 108632
Saldo akhir (idealnya 200000): 116914

Output tidak menentu karena operasi saldo += jumlah sebenarnya terdiri dari tiga langkah mesin: load dari memori ke register, tambah di register, simpan kembali ke memori. Bila scheduler menghentikan thread di antara langkah-langkah tersebut, update salah satu thread dapat tertimpa oleh thread lain. Chapter Race Condition akan membedah kasus ini secara penuh; chapter Mutex dan Lock akan menyelesaikannya.