Multi Threading

Multi Threading #

PHP dirancang sebagai bahasa single-threaded — setiap request web diproses oleh satu proses PHP yang berjalan dari awal hingga selesai, lalu dihapus dari memori. Model ini sederhana dan terbebas dari race condition, deadlock, dan kerumitan sinkronisasi yang umum di bahasa multi-threaded seperti Java atau Go. Tapi model ini juga berarti PHP secara native tidak bisa menjalankan beberapa pekerjaan secara bersamaan dalam satu proses. Artikel ini membahas bagaimana PHP menangani konkurensi dari berbagai sudut: multi-proses dengan pcntl_fork, ekstensi parallel untuk thread yang sesungguhnya, Fibers untuk konkurensi kooperatif, event loop dengan ReactPHP, dan — yang paling sering tepat di dunia nyata — job queue sebagai alternatif yang jauh lebih andal dari spawning thread manual.

Mengapa PHP Single-Threaded #

Model “shared-nothing” PHP adalah keputusan desain yang disengaja, bukan keterbatasan. Setiap request mendapat proses (atau thread) PHP sendiri yang tidak berbagi state dengan request lain. Ketika request selesai, semua variabel dan objek dihapus dari memori.

flowchart LR
    Client1[Request 1] --> PHP1[PHP Process 1\nmemori tersendiri]
    Client2[Request 2] --> PHP2[PHP Process 2\nmemori tersendiri]
    Client3[Request 3] --> PHP3[PHP Process 3\nmemori tersendiri]

    PHP1 --> DB[(Database)]
    PHP2 --> DB
    PHP3 --> DB

    PHP1 --> Done1[Response 1]
    PHP2 --> Done2[Response 2]
    PHP3 --> Done3[Response 3]

    style PHP1 fill:#dbeafe
    style PHP2 fill:#dcfce7
    style PHP3 fill:#fef9c3

Konkurensi di level web server ditangani oleh web server itu sendiri (Apache, Nginx, PHP-FPM) — bukan oleh kode PHP. PHP-FPM mengelola pool of workers, masing-masing menangani satu request pada satu waktu. Ini berarti skala horizontal (tambah server) jauh lebih mudah dari skala vertikal (buat satu proses lebih pintar).

Masalah muncul ketika satu tugas PHP butuh waktu lama dan kamu ingin melakukan beberapa hal sekaligus dalam satu eksekusi — misalnya: crawl 100 URL sekaligus, proses 1000 baris CSV secara paralel, atau menjalankan beberapa query database secara bersamaan. Untuk itulah opsi-opsi di bawah ada.


Multi-Proses dengan pcntl_fork #

Ekstensi pcntl (Process Control) memungkinkan PHP membuat proses anak menggunakan fork — teknik Unix klasik yang menyalin proses saat ini menjadi dua proses identik yang berjalan paralel.

pcntl hanya tersedia di Unix/Linux dan tidak bisa digunakan di PHP yang berjalan sebagai web server module. Ini hanya untuk skrip CLI.
<?php
declare(strict_types=1);

// Pastikan ekstensi pcntl tersedia
if (!function_exists('pcntl_fork')) {
    die("Ekstensi pcntl tidak tersedia\n");
}

$urls = [
    'https://api.example.com/data/1',
    'https://api.example.com/data/2',
    'https://api.example.com/data/3',
    'https://api.example.com/data/4',
];

$pids = []; // simpan PID semua proses anak

foreach ($urls as $i => $url) {
    $pid = pcntl_fork();

    if ($pid === -1) {
        // Fork gagal
        throw new \RuntimeException("Gagal membuat proses untuk URL $i");

    } elseif ($pid === 0) {
        // Ini adalah proses ANAK
        // Setiap anak menangani satu URL
        $data = file_get_contents($url);
        file_put_contents("/tmp/hasil_$i.json", $data);
        echo "Proses anak $i selesai: " . strlen($data) . " byte\n";
        exit(0); // PENTING: anak harus exit, tidak lanjut ke iterasi berikutnya!

    } else {
        // Ini adalah proses INDUK
        $pids[$pid] = $url;
        echo "Spawn proses anak PID $pid untuk $url\n";
    }
}

// Induk menunggu semua anak selesai
foreach ($pids as $pid => $url) {
    $status = 0;
    pcntl_waitpid($pid, $status);

    if (pcntl_wifexited($status)) {
        $exitCode = pcntl_wexitstatus($status);
        echo "PID $pid selesai dengan exit code $exitCode\n";
    }
}

echo "Semua proses anak selesai\n";

Worker Pool dengan Fork #

Untuk memproses banyak item tanpa membuat terlalu banyak proses sekaligus, gunakan pola worker pool:

<?php
declare(strict_types=1);

function prosesParalel(array $items, callable $handler, int $maxWorker = 4): void
{
    $antrian = $items;
    $aktif   = []; // PID proses yang sedang berjalan

    while (!empty($antrian) || !empty($aktif)) {
        // Spawn worker baru jika ada slot dan ada item
        while (count($aktif) < $maxWorker && !empty($antrian)) {
            $item = array_shift($antrian);
            $pid  = pcntl_fork();

            if ($pid === 0) {
                // Proses anak
                try {
                    $handler($item);
                } catch (\Throwable $e) {
                    error_log("Worker error: " . $e->getMessage());
                    exit(1);
                }
                exit(0);
            } elseif ($pid > 0) {
                $aktif[$pid] = $item;
            }
        }

        // Tunggu salah satu anak selesai sebelum spawn berikutnya
        if (!empty($aktif)) {
            $status = 0;
            $pid    = pcntl_wait($status); // tunggu child manapun
            if ($pid > 0) {
                unset($aktif[$pid]);
            }
        }
    }
}

// Penggunaan
$daftarFile = glob('/data/csv/*.csv');

prosesParalel($daftarFile, function(string $file) {
    $baris = 0;
    $handle = fopen($file, 'r');
    while (($row = fgetcsv($handle)) !== false) {
        // proses setiap baris
        $baris++;
    }
    fclose($handle);
    echo basename($file) . ": $baris baris diproses\n";
}, maxWorker: 8);

Berbagi Data Antar Proses #

Proses hasil fork tidak berbagi memori — mereka adalah salinan independen. Untuk berbagi data, gunakan salah satu mekanisme IPC (Inter-Process Communication):

<?php
// 1. Shared Memory — paling cepat
$shmKey  = ftok(__FILE__, 'a');
$shmId   = shmop_open($shmKey, 'c', 0644, 1024);

// Tulis ke shared memory
shmop_write($shmId, json_encode(['status' => 'ok', 'count' => 42]), 0);

// Baca dari shared memory (bisa dari proses berbeda)
$data = json_decode(shmop_read($shmId, 0, 1024), true);
shmop_close($shmId);

// 2. File — paling sederhana, gunakan lock
function tulisAman(string $path, string $data): void
{
    $fp = fopen($path, 'a');
    flock($fp, LOCK_EX);  // exclusive lock
    fwrite($fp, $data . "\n");
    flock($fp, LOCK_UN);  // lepas lock
    fclose($fp);
}

// 3. Database atau Redis — paling fleksibel untuk produksi

Ekstensi parallel — True Multi-Threading #

Ekstensi parallel (tersedia via PECL) memberikan PHP kemampuan multi-threading yang sesungguhnya. Thread berjalan dalam proses yang sama tapi di eksekusi paralel, berbagi eksekusi engine PHP.

Ekstensi parallel membutuhkan PHP yang dikompilasi dengan ZTS (Zend Thread Safety) aktif — biasanya PHP-ZTS. Banyak shared hosting tidak menyediakan ini. Cocok untuk aplikasi CLI atau container yang dikendalikan sendiri.
<?php
use parallel\{Runtime, Future, Channel};

// Runtime adalah thread worker
$runtime1 = new Runtime();
$runtime2 = new Runtime();

// Future mewakili nilai yang akan datang (async result)
$future1 = $runtime1->run(function(): int {
    // Kode ini berjalan di thread terpisah
    $total = 0;
    for ($i = 1; $i <= 1_000_000; $i++) {
        $total += $i;
    }
    return $total;
});

$future2 = $runtime2->run(function(): int {
    $total = 0;
    for ($i = 1_000_001; $i <= 2_000_000; $i++) {
        $total += $i;
    }
    return $total;
});

// Tunggu kedua thread selesai dan ambil hasilnya
$total = $future1->value() + $future2->value();
echo "Total: $total\n"; // 2000001000000

// Channel — komunikasi antar thread
$channel = new Channel();

$producer = new Runtime();
$producer->run(function(Channel $ch): void {
    for ($i = 0; $i < 10; $i++) {
        $ch->send("pesan-$i");
        usleep(10000); // 10ms
    }
    $ch->close();
}, [$channel]);

// Konsumsi pesan dari channel
while (true) {
    try {
        $pesan = $channel->recv();
        echo "Terima: $pesan\n";
    } catch (\parallel\Channel\Error\Closed $e) {
        break; // channel ditutup, selesai
    }
}

Parallel dengan Pool #

<?php
use parallel\{Runtime, Future};

function parallelMap(array $items, \Closure $fn, int $workers = 4): array
{
    $runtimes = [];
    $futures  = [];

    // Buat pool of runtimes
    for ($i = 0; $i < $workers; $i++) {
        $runtimes[] = new Runtime();
    }

    // Distribusi pekerjaan ke workers secara round-robin
    foreach ($items as $i => $item) {
        $runtime    = $runtimes[$i % $workers];
        $futures[$i] = $runtime->run($fn, [$item]);
    }

    // Kumpulkan semua hasil
    $hasil = [];
    foreach ($futures as $i => $future) {
        $hasil[$i] = $future->value();
    }

    return $hasil;
}

$urls = ['url1', 'url2', 'url3', 'url4', 'url5', 'url6', 'url7', 'url8'];

$hasilDownload = parallelMap($urls, function(string $url): string {
    // Simulasi download — berjalan paralel di beberapa thread
    sleep(1);
    return "Konten dari $url";
}, workers: 4);

// 8 URL yang masing-masing butuh 1 detik, dengan 4 workers: selesai dalam ~2 detik
// (dibanding 8 detik jika sequential)

Fibers — Konkurensi Kooperatif (PHP 8.1+) #

Fibers adalah cara PHP 8.1 untuk menulis kode asinkron yang terlihat synchronous. Berbeda dari thread (yang berjalan benar-benar paralel), Fiber berjalan secara kooperatif — hanya satu yang aktif setiap saat, tapi bisa di-pause dan dilanjutkan.

sequenceDiagram
    participant Main as Main Thread
    participant F1 as Fiber 1
    participant F2 as Fiber 2

    Main->>F1: start()
    F1->>F1: Eksekusi...
    F1->>Main: suspend() — "tunggu IO selesai"
    Main->>F2: start()
    F2->>F2: Eksekusi...
    F2->>Main: suspend()
    Main->>F1: resume()
    F1->>F1: Lanjut...
    F1->>Main: Selesai (return)
    Main->>F2: resume()
    F2->>Main: Selesai (return)
<?php
// Fiber dasar
$fiber = new Fiber(function(): string {
    echo "Fiber dimulai\n";

    // suspend() — pause fiber, kembalikan nilai ke pemanggil
    $nilaiDariResume = Fiber::suspend('hasil suspend pertama');
    echo "Dilanjutkan dengan: $nilaiDariResume\n";

    Fiber::suspend('hasil suspend kedua');
    echo "Fiber selesai\n";

    return 'nilai return akhir';
});

// start() — mulai fiber, berjalan sampai suspend() pertama
$nilai1 = $fiber->start();
echo "Suspend pertama: $nilai1\n"; // "hasil suspend pertama"

// resume() — lanjutkan fiber, berjalan sampai suspend() berikutnya
$nilai2 = $fiber->resume('data dari main');
echo "Suspend kedua: $nilai2\n";   // "hasil suspend kedua"

// resume() terakhir — fiber berjalan sampai selesai
$fiber->resume();
echo "Return: " . $fiber->getReturn() . "\n"; // "nilai return akhir"

Fibers untuk Simulasi Konkurensi #

Fibers tidak memberikan paralelisme nyata (CPU masih mengeksekusi satu hal pada satu waktu), tapi sangat berguna untuk menulis kode asinkron yang bersih — terutama dikombinasikan dengan event loop:

<?php
class SimpleEventLoop
{
    private array $fibers = [];
    private array $antrian = [];

    public function tambah(Fiber $fiber): void
    {
        $this->fibers[] = $fiber;
    }

    public function jadwalkan(callable $fn): void
    {
        $this->tambah(new Fiber($fn));
    }

    public function jalankan(): void
    {
        // Mulai semua fiber
        foreach ($this->fibers as $fiber) {
            if (!$fiber->isStarted()) {
                $fiber->start();
            }
        }

        // Terus jalankan sampai semua selesai
        while (true) {
            $masihAktif = false;
            foreach ($this->fibers as $fiber) {
                if ($fiber->isSuspended()) {
                    $fiber->resume();
                    $masihAktif = true;
                } elseif (!$fiber->isTerminated()) {
                    $masihAktif = true;
                }
            }
            if (!$masihAktif) break;
        }
    }
}

$loop = new SimpleEventLoop();

$loop->jadwalkan(function(): void {
    echo "Task 1: mulai\n";
    Fiber::suspend();
    echo "Task 1: lanjut\n";
    Fiber::suspend();
    echo "Task 1: selesai\n";
});

$loop->jadwalkan(function(): void {
    echo "Task 2: mulai\n";
    Fiber::suspend();
    echo "Task 2: selesai\n";
});

$loop->jalankan();
// Task 1: mulai
// Task 2: mulai
// Task 1: lanjut
// Task 2: selesai
// Task 1: selesai

ReactPHP — Event Loop yang Matang #

Untuk aplikasi yang butuh I/O asinkron yang sungguh-sungguh (server socket, HTTP client non-blocking), ReactPHP adalah library yang paling matang untuk PHP. Ia mengimplementasikan event loop Reactor pattern.

composer require react/event-loop react/http react/promise
<?php
require 'vendor/autoload.php';

use React\EventLoop\Loop;
use React\Http\Browser;
use React\Promise\Promise;

// Browser ReactPHP adalah HTTP client non-blocking
$browser = new Browser();

// Fetch beberapa URL secara paralel (non-blocking)
$promises = [
    $browser->get('https://api.example.com/users'),
    $browser->get('https://api.example.com/products'),
    $browser->get('https://api.example.com/orders'),
];

// React\Promise\all() — tunggu semua promise selesai
\React\Promise\all($promises)->then(
    function(array $responses): void {
        foreach ($responses as $i => $response) {
            $data = json_decode((string)$response->getBody(), true);
            echo "Response $i: " . count($data) . " item\n";
        }
    },
    function(\Throwable $e): void {
        echo "Error: " . $e->getMessage() . "\n";
    }
);

// Event loop berjalan sampai semua operasi async selesai
Loop::run();

HTTP Server dengan ReactPHP #

<?php
require 'vendor/autoload.php';

use React\Http\HttpServer;
use React\Http\Message\Response;
use React\Socket\SocketServer;
use Psr\Http\Message\ServerRequestInterface;

$server = new HttpServer(function(ServerRequestInterface $request): Response {
    $path = $request->getUri()->getPath();

    return match(true) {
        $path === '/'         => Response::plaintext("Halo dari ReactPHP!\n"),
        $path === '/api/ping' => Response::json(['status' => 'ok', 'timestamp' => time()]),
        default               => new Response(404, [], "Tidak ditemukan\n"),
    };
});

$socket = new SocketServer('0.0.0.0:8080');
$server->listen($socket);

echo "Server berjalan di http://localhost:8080\n";

\React\EventLoop\Loop::run();

ReactPHP memungkinkan satu PHP process menangani ribuan koneksi bersamaan (seperti Node.js) — sangat berbeda dari model request-per-process PHP tradisional.


Job Queue — Alternatif Paling Praktis #

Untuk sebagian besar kebutuhan konkurensi di aplikasi web PHP nyata, job queue adalah solusi yang jauh lebih tepat dari multi-threading manual. Alih-alih membuat thread dalam satu proses, kamu melempar pekerjaan ke queue dan membiarkan worker pool yang terpisah memprosesnya secara paralel.

flowchart LR
    App[Aplikasi Web\nPHP Request] -- "dispatch job" --> Queue[(Queue\nRedis/Database)]
    Queue --> W1[Worker 1\nphp artisan queue:work]
    Queue --> W2[Worker 2\nphp artisan queue:work]
    Queue --> W3[Worker 3\nphp artisan queue:work]
    W1 --> Done[Pekerjaan\nSelesai]
    W2 --> Done
    W3 --> Done

    style Queue fill:#fef9c3
    style Done fill:#dcfce7

Implementasi Sederhana dengan Redis #

<?php
// Job sederhana — kelas yang mendeskripsikan pekerjaan
class KirimEmailJob
{
    public function __construct(
        public readonly string $ke,
        public readonly string $subjek,
        public readonly string $isi,
    ) {}

    public function handle(): void
    {
        // Logika pengiriman email
        $mailer = new SmtpMailer();
        $mailer->kirim($this->ke, $this->subjek, $this->isi);
        echo "Email terkirim ke {$this->ke}\n";
    }
}

// Queue sederhana menggunakan Redis
class SimpleQueue
{
    private \Redis $redis;

    public function __construct()
    {
        $this->redis = new \Redis();
        $this->redis->connect('127.0.0.1', 6379);
    }

    public function dispatch(object $job): void
    {
        $payload = serialize($job);
        $this->redis->rpush('queue:default', $payload);
        echo "Job " . get_class($job) . " masuk ke queue\n";
    }

    public function proses(): void
    {
        echo "Worker mulai berjalan...\n";

        while (true) {
            // BLPOP — blocking pop, tunggu sampai ada item
            $result = $this->redis->blpop(['queue:default'], 5); // timeout 5 detik

            if ($result === null) {
                continue; // timeout, coba lagi
            }

            $payload = $result[1];
            $job     = unserialize($payload);

            try {
                $job->handle();
                echo "Job " . get_class($job) . " berhasil\n";
            } catch (\Throwable $e) {
                error_log("Job gagal: " . $e->getMessage());
                // Bisa push ke queue 'failed' untuk retry nanti
                $this->redis->rpush('queue:failed', $payload);
            }
        }
    }
}

// Di controller/handler web — dispatch job
$queue = new SimpleQueue();
$queue->dispatch(new KirimEmailJob(
    ke:      '[email protected]',
    subjek:  'Selamat Datang!',
    isi:     'Terima kasih telah mendaftar.',
));
// Request selesai instan — email dikirim oleh worker, bukan request ini

// Di worker (CLI terpisah, dijalankan sebagai daemon)
// php worker.php
$queue = new SimpleQueue();
$queue->proses();

Dengan Supervisord — Kelola Worker sebagai Daemon #

; /etc/supervisor/conf.d/queue-worker.conf
[program:php-queue-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/worker.php
directory=/var/www/app
autostart=true
autorestart=true
numprocs=4          ; 4 worker berjalan paralel
redirect_stderr=true
stdout_logfile=/var/log/queue-worker.log
# Kelola worker dengan supervisord
supervisorctl reread
supervisorctl update
supervisorctl start php-queue-worker:*
supervisorctl status

Kapan Butuh Konkurensi dan Kapan Tidak #

Butuh konkurensi jika:
  ✓ Proses batch yang bisa diparalel (konversi gambar, parse CSV besar)
  ✓ Fetch banyak API eksternal secara bersamaan
  ✓ Tugas background yang tidak perlu response instan (kirim email, notifikasi)
  ✓ Server yang harus menangani ribuan koneksi persisten (chat, streaming)

TIDAK butuh konkurensi jika:
  ✗ Aplikasi web biasa — PHP-FPM sudah handle konkurensi di level server
  ✗ Query database lambat — optimalkan query, bukan buat thread
  ✗ Kode yang lambat karena algoritma buruk — perbaiki algoritmanya
  ✗ "Biar lebih cepat" tanpa profiling — premature optimization

Pilih solusi yang tepat:
  Job Queue     → tugas async yang tidak butuh hasil instan (85% kasus)
  pcntl_fork    → CLI script dengan banyak unit kerja independent
  parallel      → CPU-intensive computation di CLI/daemon
  ReactPHP      → server yang butuh ribuan koneksi bersamaan
  Fibers        → kode async yang bersih tanpa callback hell

Ringkasan #

  • PHP single-threaded by design — model shared-nothing membuat PHP aman dari race condition. Konkurensi di level web ditangani oleh PHP-FPM, bukan kode PHP.
  • pcntl_fork membuat proses anak yang independen — paling sederhana untuk paralelisasi CLI script. Proses anak tidak berbagi memori dengan induk; gunakan file, shared memory, atau database untuk komunikasi.
  • Ekstensi parallel memberikan true multi-threading via Runtime, Future, dan Channel — butuh PHP-ZTS dan cocok untuk CPU-intensive task di CLI.
  • Fibers (PHP 8.1+) untuk konkurensi kooperatif — bukan paralelisme nyata, tapi memungkinkan penulisan kode async yang bersih dan mudah dibaca.
  • ReactPHP untuk I/O async yang sesungguhnya — event loop yang memungkinkan satu proses menangani ribuan koneksi seperti Node.js.
  • Job Queue adalah solusi paling praktis untuk 90% kebutuhan konkurensi aplikasi web — dispatch job ke queue, worker pool yang terpisah memprosesnya secara paralel. Gunakan Supervisord untuk mengelola worker sebagai daemon.
  • Jangan tambah konkurensi sebelum profiling — query database yang lambat, algoritma O(n²), atau kurang cache jauh lebih sering menjadi penyebab lambat daripada kurangnya thread.

← Sebelumnya: Vendoring   Berikutnya: I/O →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact