Многопоточность в PHP

Есть много задач, требующих многопоточности, и меня заинтересовало как реализован этот механизм в PHP. Однако, эта тема довольно слабо раскрыта. В частности, на сайте php.net очень мало информации о том, что происходит, когда создается дочерний процесс. Я решил провести несколько тестов для изучения этого вопроса.

Итак, объект эксперимента — расширение pcntl. Здесь меня интересовало два основных вопроса: как процессы видят переменные в памяти и как они эту самую память расходуют. Для теста я написал небольшой скрипт.

#!/usr/bin/php -q 
<?php

error_reporting(E_ALL); 
set_time_limit(0); 
ob_implicit_flush(); 
declare(ticks = 1);

change_identity(65534, 65534); 

pcntl_signal(SIGTERM, 'sig_handler'); 
pcntl_signal(SIGINT, 'sig_handler'); 
pcntl_signal(SIGCHLD, 'sig_handler'); 

class Test
{
	public $counter = 0;
	public $pids = array();
	public $data;
	
	public function loaddata($file)
	{
		$this->data = file_get_contents($file);
		echo "Loaded: " . strlen($this->data) . " B\n";
	}
	
	public function fork()
	{
		$this->counter++;
		$pid = pcntl_fork();
		if ($pid == -1) { 
			die("Fork failure ! \n");
		} elseif ($pid) {
			$this->pids[] = $pid;
		} else {
			echo "PID created, counter: " . $this->counter . " \n";
			$this->counter++;
			echo "Datasize: " . strlen($this->data) . " B \n";
			$this->loaddata('data2');
			sleep(10);
			echo "Datasize: " . strlen($this->data) . " B \n";
			echo "PID done, counter: " . $this->counter . " \n";
			exit();
		}
	}
	
	public function waitall()
	{
		foreach ($this->pids as $pid) {
			pcntl_waitpid($pid, $status);
		}
	}
}

$t = new Test();

$t->loaddata('data1');

for ($i = 0; $i < 10; $i++) {
	$t->fork();
	sleep(1);
}

echo "Wait for the children \n";
$t->waitall();
echo "All children done \n";

function change_identity($uid, $gid)
{
    if(!posix_setgid($gid)) { 
        die("Unable to setgid to $gid ! \n"); 
    } 

    if(!posix_setuid($uid)) { 
        die("Unable to setuid to $uid ! \n"); 
    } 
}

function sig_handler($sig) 
{ 
    switch($sig) 
    { 
        case SIGTERM: 
        case SIGINT: 
            exit(); 
        break; 

        case SIGCHLD: 
            pcntl_waitpid(-1, $status);
        break; 
    } 
}

Что делает этот скрипт? Он загружает в память большой файл data1 (более 5M) и создает несколько дочерних процессов. При создании процесса в главном потоке увеличивается счетчик процессов, внутри дочернего процесса мы загружаем файл поменьше data2 (до 1M), пытаемся еще раз увеличить счетчик и выводим результаты. Результаты работы скрипта такие:

Loaded: 6318476 B
PID created, counter: 1 
Datasize: 6318476 B 
Loaded: 429124 B
PID created, counter: 2 
Datasize: 6318476 B 
Loaded: 429124 B
PID created, counter: 3 
Datasize: 6318476 B 
Loaded: 429124 B
PID created, counter: 4 
Datasize: 6318476 B 
Loaded: 429124 B
PID created, counter: 5 
Datasize: 6318476 B 
Loaded: 429124 B
PID created, counter: 6 
Datasize: 6318476 B 
Loaded: 429124 B
PID created, counter: 7 
Datasize: 6318476 B 
Loaded: 429124 B
PID created, counter: 8 
Datasize: 6318476 B 
Loaded: 429124 B
PID created, counter: 9 
Datasize: 6318476 B 
Loaded: 429124 B
PID created, counter: 10 
Datasize: 6318476 B 
Loaded: 429124 B
Wait for the children 
Datasize: 429124 B 
PID done, counter: 2 
Datasize: 429124 B 
PID done, counter: 3 
Datasize: 429124 B 
PID done, counter: 4 
Datasize: 429124 B 
PID done, counter: 5 
Datasize: 429124 B 
PID done, counter: 6 
Datasize: 429124 B 
PID done, counter: 7 
Datasize: 429124 B 
PID done, counter: 8 
Datasize: 429124 B 
PID done, counter: 9 
Datasize: 429124 B 
PID done, counter: 10 
Datasize: 429124 B 
PID done, counter: 11 
All children done 

В ходе работы скрипта я также наблюдал за выделением памяти через системный монитор. В результате всех этих манипуляций я выяснил, что:

  • дочерний процесс имеет доступ ко всем переменным родителя (в пределах scope, естественно);
  • если дочерний процесс пытается поменять какие-либо данные, на родителе это не отражается, изменения видны только внутри данного процесса;
  • если родитель меняет данные после создания дочернего процесса, то «ребенок» этого так же не видит;
  • после создания дочернего процесса выделение памяти происходит только при создании новых объектов (переменных), а копирование данных родителя не происходит.

Отсюда следует примерно следующее: после создания дочернего процесса, родитель передает ему ссылки на все данные, которые были накоплены ранее, с которых он (дочерний процесс) может либо считать данные, либо переприсвоить ссылку на другую область данных. То же касается и родителя, если он меняет свои данные, то он переносит свой указатель на новую область данных, а старая переходит либо ребенку, либо мусорщику. Поэтому, если требуется работать с общими данными в нескольких процессах, нужно использовать shared memory и/или семафоры, но это уже другая тема.