Jeśli mówimy o PHP, wielowątkowość jest jedną z ostatnich rzeczy jaka przychodzi nam do głowy. Po części dlatego, że język ten od zawsze miał problem z tym zagadnieniem. Jak się okazuje, istnieje rozszerzenie rozwijane od dobrych kilku lat, które zadaje kłam twierdzeniu, że w PHP nie ma wielowątkowości (lub jest ale bardzo ułomna).

Zanim zaczniemy, będziemy musieli przygotować środowisko. Niestety większość instalacji PHP nie nadaje się do korzystania z pthreads, ponieważ rozszerzenie wymaga aby język był skompilowany jako thread safe (ZTS). Ponadto będziemy potrzebować PEAR, którego użyjemy do instalacji pthreads, dostępnego jako paczka PECL.

Mając gotowe środowisko, możemy przystąpić do pisania kodu. Zaczniemy od klasy Thread. Klasa ta pozwala na równoległe wykonywanie kodu PHP. Za równoległe wykonanie kodu odpowiada metoda run, której zawartość zostanie wykonana w osobnym wątku.

Z oczywistych względów, zaprezentowane przykłady będą bardzo uproszczone. Ich zadaniem jest zaprezentowanie idei wielowątkowości w PHP.

class Logger extends \Thread
{
    protected $event;

    public function __construct($event)
    {
        $this->event = $event;
    }

    public function run()
    {
        echo 'processing: '.$this->event.PHP_EOL;
        sleep(rand(1, 5));
        echo 'finished: '.$this->event.PHP_EOL;
    }
}

Zadaniem powyższej klasy jest zalogowanie wydarzenia zgłoszonego przez aplikację. Jeśli klasa nie dziedziczyłaby po Thread, wówczas istnieje spora szansa, że logowanie zdarzenia spowoduje zablokowanie całej aplikacji na dłuższy czas. Na szczęście możemy wykorzystać możliwość uruchomienia logowania w osobnym wątku. Odbywa się to poprzez wywołanie metody start na obiekcie naszego loggera. Metoda to uruchomi w osobnym wątku metodę run.

echo 'start main thread'.PHP_EOL;

$events = ['some event', 'another event', 'error'];

foreach ($events as &$event) {
    $event = new Logger($event);
    $event->start();
}

echo 'end main thread'.PHP_EOL;

Wynik powyższego kodu będzie wyglądał następująco (kolejność może się różnić, w zależności od wylosowanej wartości przekazanej do funkcji sleep.

start main thread
processing: some event
processing: another event
processing: error
end main thread
finished: another event
finished: error
finished: some event

Jeśli chcemy aby główny wątek poczekał z dalszym wykonywaniem instrukcji, możemy użyć metody join. Spowoduje ona, że wykonywanie programu zostanie wznowione dopiero, gdy wszystkie wątki poboczne zostaną zakończone.

echo 'start main thread'.PHP_EOL;

$events = ['some event', 'another event', 'error'];

foreach ($events as &$event) {
    $event = new Logger($event);
    $event->start();
}

foreach ($events as &$event) {
    $event->join();
}

echo 'end main thread'.PHP_EOL;

W efekcie uzyskamy następujący wynik.

start main thread
processing: some event
processing: another event
processing: error
finished: error
finished: some event
finished: another event
end main thread

Jak widać program wstrzymał się z wyświetleniem end main thread do czasu zakończenia pracy wszystkich wątków.

Ręczne tworzenie wątków, ich uruchamianie oraz włączanie do głównego wątku, mimo iż trudne nie jest, do najwygodniejszych nie należy. Na szczęście mamy do dyspozycji klasę Worker, która automatyzuje ten proces.

Wystarczy, że przekażemy do workera obiekt naszego loggera, a ten zostanie wykonany w osobnym wątku. Jeśli przekażemy więcej instancji loggera, zostaną one wykonane po kolei. PHP nie ogranicza nas do jednego workera, więc jeśli chcemy skorzystać z większej ilości wątków, nic nie stoi na przeszkodzie.

echo 'start main thread'.PHP_EOL;

$worker1 = new \Worker();
$worker2 = new \Worker();

$worker1->stack(new Logger('some event'));
$worker1->stack(new Logger('another event'));

$worker2->stack(new Logger('error'));

$worker1->start();
$worker2->start();

$worker1->shutdown();
$worker2->shutdown();

echo 'end main thread'.PHP_EOL;

Powyższy kod zwróci wynik podobny do poniższego. Metoda shutdown działa identycznie jak join i jej obecność spowoduje, że główny wątek będzie czekał na zakończenie pozostałych wątków.

start main thread
processing: some event
processing: error
finished: some event
finished: error
processing: another event
finished: another event
end main thread

Mimo iż kod jest nieco bardziej przyjazny, nadal jesteśmy skazani na ręczne tworzenie workerów do obsługi naszych zadań. Z pomocą przychodzi kolejna klasa – Pool.

echo 'start main thread'.PHP_EOL;

$pool = new \Pool(10, \Worker::class);
$pool->submit(new Logger('some event'));
$pool->submit(new Logger('another event'));
$pool->submit(new Logger('error'));

$pool->shutdown();

echo 'end main thread'.PHP_EOL;

Pierwszy argument przekazany do konstruktora oznacza ilość workerów, które mają pracować dla nas, drugim jest nazwa klasy workera przez nas używanego.
Następnie poprzez metodę submit dodajemy kolejne zadania do wykonania. Na sam koniec czekamy na zakończenie wszystkich wątków. W wyniku działania powyższego kodu, uzyskamy na wyjściu coś takiego:

start main thread
processing: some event
processing: another event
processing: error
finished: another event
finished: error
finished: some event
end main thread

Ostatnim zagadnieniem jakie poruszę w temacie wątków, będzie ich synchronizacja oraz powiadomienia jakie możemy między wątkami wysyłać. Wszystko sprowadza się do wykorzystania metody synchronized, która wyśle wiadomość z jednego wątku do drugiego.

class Logger extends \Thread
{
    public function run()
    {
        echo 'logger thread stuff...'.PHP_EOL;

        $x = rand(1, 1000);

        $this->synchronized(function () use ($x) {
            $this->someNotification = 'some data: '.$x;
            $this->notify();
        });
    }
}

$logger = new Logger();
$logger->start();

echo 'main thread doing some stuff...'.PHP_EOL;

$logger->synchronized(function ($logger) {
    $logger->wait();
    echo 'notification from thread:'.PHP_EOL;
    echo $logger->someNotification.PHP_EOL;
}, $logger);

echo 'end main thread'.PHP_EOL;

Wynik działania powyższego kodu będzie wyglądał podobnie do poniższego przykładu

main thread doing some stuff...
logger thread stuff...
notification from thread:
some data: 670
end main thread

Powyższy opis nie pokrywa wszystkich tematów związanych z wątkami. W kolejnych wpisach poświęcę więcej czasu na opisanie poszczególnych klas oraz przypadków ich użycia.