Многопоточность и асинхронность в PHP: подробное руководство
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты — повар в ресторане. У тебя есть одна плита (это процессор) и один набор кастрюль (это память). Если ты готовишь одно блюдо за другим — сначала суп, потом салат, потом десерт — это называется последовательное выполнение. Ты стоишь у плиты, ждешь, пока закипит суп, потом режешь салат, потом делаешь десерт. Всё по очереди. Это медленно, но просто.
А теперь представь, что у тебя есть помощники. Один помощник следит за супом, второй режет салат, третий взбивает крем для десерта. Ты можешь давать им задания и заниматься своим делом. Это многопоточность — несколько задач выполняются одновременно (на самом деле — очень быстро переключаясь).
Но есть и другой вариант. Ты ставишь суп на плиту, включаешь таймер и уходишь резать салат. Когда таймер звонит — ты возвращаешься к супу. Ты не ждешь, а занимаешься другими делами, пока суп варится. Это асинхронность — ты не блокируешься в ожидании, а переключаешься на другую работу, когда текущая задача ждет (например, ответа от базы данных или файла).
Важно! Многопоточность — это про параллельное выполнение (реальное или иллюзорное). Асинхронность — про неблокирующее выполнение: ты не ждешь, а делаешь что-то другое, пока задача "висит" в ожидании.
В PHP исторически сложилось так, что язык был создан для веба: пришел запрос — PHP обработал его — отдал ответ — умер. Один запрос — один процесс. Но современные приложения требуют большего: обработка тысяч запросов, работа с веб-сокетами, парсинг больших файлов, параллельные вычисления. Для этого в PHP появились инструменты многопоточности и асинхронности.
Теория: как это работает изнутри
Процессы и потоки
Представь, что компьютер — это фабрика. Процесс — это цех со своими станками и рабочими. У каждого цеха своя территория (память), свои инструменты. Поток — это рабочий внутри цеха. Рабочие делят между собой станки и инструменты, но работают на одной территории.
В PHP есть три уровня параллелизма:
- Процессы (pcntl_fork) — создаются копии PHP-скрипта. Каждая копия имеет свою память. Это как открыть 10 вкладок браузера с одним сайтом.
- Потоки (pthreads) — несколько потоков внутри одного процесса. Они делят память, но могут конфликтовать (как два повара, тянущихся к одной сковороде).
- Асинхронность (ReactPHP, Amp, Swoole) — один поток, но он не ждет. Он делает одно дело, потом другое, потом возвращается к первому. Как жонглер: он не ждет, пока мяч упадет, а сразу переключается на другой.
Как работает асинхронность в PHP
В обычном PHP код выполняется строка за строкой. Если встречается sleep(5) или file_get_contents('http://...') — скрипт замирает на 5 секунд или ждет ответа от сервера. Это называется блокирующий ввод-вывод.
В асинхронном PHP используется цикл событий (Event Loop). Представь диспетчера, который раздает задачи и проверяет, не выполнилась ли какая-то из них. Диспетчер бегает по кругу и спрашивает: "Суп готов? Нет. Салат готов? Нет. А суп? Нет. А суп? Да! Забирай суп и давай новую задачу".
Цикл событий работает так:
- Запускается задача (например, запрос к базе данных).
- PHP говорит: "Я не буду ждать. Я пойду делать другие дела".
- Когда база данных отвечает, цикл событий говорит: "Эй, задача завершена! Продолжай с того места, где остановился".
Это позволяет одному PHP-процессу обрабатывать тысячи одновременных соединений — как официант, который принимает заказы у 20 столиков, не застревая ни у одного.
Многопоточность vs Асинхронность: что выбрать?
| Характеристика | Многопоточность (pthreads) | Асинхронность (ReactPHP) |
|---|---|---|
| Сложность | Высокая (гонки, блокировки, deadlock) | Средняя (нужно понимать промисы и колбэки) |
| Потребление памяти | Высокое (каждый поток — копия памяти) | Низкое (один процесс, одна память) |
| Производительность CPU | Высокая (реально использует много ядер) | Средняя (одно ядро, но много задач) |
| Подходит для | Сложных вычислений, обработки видео | Сетевых запросов, чатов, API |
| Стабильность | Риск утечек и падений | Стабильнее (один поток — проще отладка) |
Синтаксис в PHP: все формы и варианты
1. Процессы: pcntl_fork()
Это системный вызов, который создает дочерний процесс. Дочерний процесс — это точная копия родительского, но с другим PID. Работает только в CLI (командной строке), не в вебе.
// Создаем дочерний процесс
$pid = pcntl_fork();
if ($pid == -1) {
// Ошибка: не удалось создать процесс
die("Не удалось создать процесс");
} elseif ($pid) {
// Мы в родительском процессе
// $pid содержит ID дочернего процесса
echo "Родитель: жду дочерний процесс...
";
pcntl_wait($status); // Ждем завершения дочернего процесса
echo "Родитель: дочерний процесс завершен
";
} else {
// Мы в дочернем процессе
// $pid == 0
echo "Дочерний процесс: работаю...
";
sleep(2); // Имитация работы
echo "Дочерний процесс: завершил работу
";
exit(0); // Завершаем дочерний процесс
}
2. Потоки: pthreads (расширение parallel)
Современный способ — использовать расширение parallel. Оно создает потоки, которые могут обмениваться данными через каналы.
// Установка: pecl install parallel
use parallelRuntime;
use parallelChannel;
// Создаем канал для обмена данными
$channel = Channel::make('my_channel');
// Создаем новый поток (Runtime)
$runtime = new Runtime();
// Запускаем задачу в потоке
$future = $runtime->run(function($channel) {
// Это выполняется в отдельном потоке
echo "Поток: начал работу
";
sleep(2); // Имитация долгой работы
$channel->send("Результат из потока!");
echo "Поток: завершил работу
";
}, [$channel]);
// Родительский поток продолжает работать
echo "Главный поток: делаю свои дела...
";
// Получаем результат из канала (блокируется, пока не придет)
$result = $channel->recv();
echo "Главный поток: получил результат: $result
";
// Ждем завершения потока
$future->value();
3. Асинхронность: ReactPHP
ReactPHP — это библиотека для асинхронного программирования. Она использует цикл событий и неблокирующие операции.
// Установка: composer require react/event-loop react/http
use ReactEventLoopFactory;
use ReactHttpBrowser;
use PsrHttpMessageResponseInterface;
// Создаем цикл событий
$loop = Factory::create();
// Создаем HTTP-клиент (неблокирующий)
$browser = new Browser($loop);
echo "Начинаем запросы...
";
// Отправляем первый запрос (не ждем ответа!)
$browser->get('https://api.example.com/data1')
->then(function (ResponseInterface $response) {
echo "Получен ответ 1: " . $response->getBody() . "
";
});
// Отправляем второй запрос (тоже не ждем!)
$browser->get('https://api.example.com/data2')
->then(function (ResponseInterface $response) {
echo "Получен ответ 2: " . $response->getBody() . "
";
});
echo "Запросы отправлены, ждем ответов...
";
// Запускаем цикл событий
$loop->run();
echo "Все ответы получены!
";
4. Асинхронность: Amp (библиотека)
Amp использует корутины и генераторы для асинхронности. Код выглядит почти как синхронный, но работает асинхронно.
// Установка: composer require amphp/amp amphp/http-client
use AmpLoop;
use AmpHttpClientHttpClientBuilder;
use AmpHttpClientRequest;
// Асинхронная функция (корутина)
function fetchData($url) {
$client = HttpClientBuilder::buildDefault();
$request = new Request($url);
// await-подобная конструкция: yield
$response = yield $client->request($request);
$body = yield $response->getBody()->buffer();
return $body;
}
// Главная функция
Loop::run(function () {
// Запускаем две корутины параллельно
$promise1 = Ampcall(fetchData(...), ['https://api.example.com/data1']);
$promise2 = Ampcall(fetchData(...), ['https://api.example.com/data2']);
// Ждем обе (параллельно!)
list($result1, $result2) = yield AmpPromiseall([$promise1, $promise2]);
echo "Результат 1: $result1
";
echo "Результат 2: $result2
";
});
5. Swoole: серверный подход
Swoole — это расширение для PHP, которое превращает PHP в асинхронный сервер. Оно поддерживает корутины, веб-сокеты, TCP/UDP серверы.
// Установка: pecl install swoole
use SwooleCoroutine;
use function SwooleCoroutine
un;
// Запускаем корутины
run(function () {
// Создаем корутину 1
Coroutine::create(function () {
echo "Корутина 1: начало
";
Coroutine::sleep(2); // Неблокирующий сон
echo "Корутина 1: конец
";
});
// Создаем корутину 2
Coroutine::create(function () {
echo "Корутина 2: начало
";
Coroutine::sleep(1); // Неблокирующий сон
echo "Корутина 2: конец
";
});
echo "Главная корутина: все запущено
";
});
Примеры кода — от простого к сложному
Пример 1: Синхронный запрос (проблема)
// Проблема: каждый запрос ждет предыдущий
echo "Начало: " . date('H:i:s') . "
";
// Первый запрос — ждем 2 секунды
$data1 = file_get_contents('https://httpbin.org/delay/2');
echo "Получен ответ 1: " . date('H:i:s') . "
";
// Второй запрос — ждем еще 2 секунды
$data2 = file_get_contents('https://httpbin.org/delay/2');
echo "Получен ответ 2: " . date('H:i:s') . "
";
// Итого: 4 секунды
echo "Конец: " . date('H:i:s') . "
";
Пример 2: Асинхронные запросы с ReactPHP
// Установка: composer require react/http react/event-loop
use ReactEventLoopFactory;
use ReactHttpBrowser;
require 'vendor/autoload.php';
$loop = Factory::create();
$browser = new Browser($loop);
echo "Начало: " . date('H:i:s') . "
";
// Отправляем оба запроса одновременно
$browser->get('https://httpbin.org/delay/2')->then(function ($response) {
echo "Получен ответ 1: " . date('H:i:s') . "
";
});
$browser->get('https://httpbin.org/delay/2')->then(function ($response) {
echo "Получен ответ 2: " . date('H:i:s') . "
";
});
echo "Запросы отправлены: " . date('H:i:s') . "
";
$loop->run();
// Итого: ~2 секунды (оба запроса выполняются параллельно)
echo "Конец: " . date('H:i:s') . "
";
Пример 3: Параллельные вычисления с parallel
// Установка: pecl install parallel
use parallelRuntime;
// Функция для вычисления в потоке
function calculateFactorial($n) {
$result = 1;
for ($i = 2; $i <= $n; $i++) {
$result *= $i;
}
return $result;
}
// Создаем потоки
$runtimes = [];
$futures = [];
// Запускаем 3 потока для вычисления факториалов
for ($i = 1; $i <= 3; $i++) {
$runtime = new Runtime();
$runtimes[] = $runtime;
$futures[] = $runtime->run(function($n) {
return calculateFactorial($n);
}, [$i * 10]); // 10!, 20!, 30!
}
// Собираем результаты
foreach ($futures as $index => $future) {
$result = $future->value();
echo "Факториал " . (($index + 1) * 10) . "! = $result
";
}
Пример 4: Асинхронный TCP-сервер
// Установка: composer require react/socket
use ReactEventLoopFactory;
use ReactSocketServer;
use ReactSocketConnectionInterface;
$loop = Factory::create();
// Создаем TCP-сервер на порту 8080
$server = new Server('127.0.0.1:8080', $loop);
echo "Сервер запущен на 127.0.0.1:8080
";
// Обрабатываем каждое подключение
$server->on('connection', function (ConnectionInterface $connection) {
echo "Новое подключение: " . $connection->getRemoteAddress() . "
";
// Отправляем приветствие
$connection->write("Добро пожаловать на сервер!
");
// Обрабатываем входящие данные
$connection->on('data', function ($data) use ($connection) {
echo "Получено: " . trim($data) . "
";
$connection->write("Вы сказали: " . $data);
});
// Обрабатываем отключение
$connection->on('close', function () {
echo "Подключение закрыто
";
});
});
$loop->run();
Пример 5: Асинхронная работа с файлами
// Установка: composer require react/event-loop react/filesystem
use ReactEventLoopFactory;
use ReactFilesystemFilesystem;
$loop = Factory::create();
$filesystem = Filesystem::create($loop);
echo "Начинаем чтение файлов...
";
// Читаем несколько файлов асинхронно
$files = ['file1.txt', 'file2.txt', 'file3.txt'];
foreach ($files as $file) {
$filesystem->file($file)->getContents()->then(function ($contents) use ($file) {
echo "Прочитан файл $file: " . substr($contents, 0, 50) . "...
";
});
}
echo "Чтение запущено, ждем...
";
$loop->run();
Пример 6: Обработка ошибок в асинхронном коде
use ReactEventLoopFactory;
use ReactHttpBrowser;
use ReactPromisePromiseInterface;
$loop = Factory::create();
$browser = new Browser($loop);
// Функция для безопасного запроса
function safeRequest($browser, $url) {
return $browser->get($url)
->then(
function ($response) {
// Успех
return $response->getBody();
},
function ($error) {
// Ошибка
echo "Ошибка при запросе: " . $error->getMessage() . "
";
return "Ошибка: " . $error->getMessage();
}
);
}
// Отправляем запросы с обработкой ошибок
safeRequest($browser, 'https://httpbin.org/delay/1')
->then(function ($result) {
echo "Результат 1: $result
";
});
safeRequest($browser, 'https://несуществующий-сайт.xyz')
->then(function ($result) {
echo "Результат 2: $result
";
});
$loop->run();
Пример 7: Комбинирование многопоточности и асинхронности
// Установка: pecl install parallel, composer require react/event-loop
use parallelRuntime;
use ReactEventLoopFactory;
use ReactHttpBrowser;
// Создаем цикл событий в главном потоке
$loop = Factory::create();
$browser = new Browser($loop);
// Функция для выполнения в отдельном потоке
function heavyComputation($data) {
// Тяжелые вычисления (CPU-bound)
$result = 0;
for ($i = 0; $i < 1000000; $i++) {
$result += sin($i) * cos($i);
}
return "Результат вычислений: $result для данных: $data";
}
// Запускаем тяжелые вычисления в потоках
$runtime = new Runtime();
$future = $runtime->run(function($data) {
return heavyComputation($data);
}, ['тестовые данные']);
// Асинхронно делаем HTTP-запрос
$browser->get('https://httpbin.org/delay/1')
->then(function ($response) {
echo "HTTP-запрос завершен: " . $response->getBody() . "
";
});
// Ждем результат из потока
$result = $future->value();
echo "Результат из потока: $result
";
// Запускаем цикл событий для обработки HTTP
$loop->run();
Сравнение с Python: чем похоже и чем отличается
| Аспект | PHP | Python |
|---|---|---|
| Встроенная многопоточность | Есть (pthreads/parallel), но нестабильна | Есть (threading), но GIL ограничивает |
| Асинхронность в ядре | Нет (только через библиотеки) | Есть (asyncio с Python 3.4+) |
| Популярные библиотеки | ReactPHP, Amp, Swoole | asyncio, aiohttp, trio |
| Синтаксис асинхронности | Колбэки, промисы (ReactPHP) или генераторы (Amp) | async/await (естественный) |
| Производительность | Выше для веб-серверов (Swoole) | Ниже из-за GIL, но asyncio быстр |
| Сложность обучения | Выше (нет единого стандарта) | Ниже (единый asyncio) |
| Использование в вебе | Традиционно синхронный, Swoole меняет это | Часто асинхронный (FastAPI, aiohttp) |
Ключевое отличие: В Python асинхронность — это часть языка (async/await). В PHP это надстройка через библиотеки. Python проще для новичков в асинхронности, но PHP (с Swoole) может быть быстрее в production.
Типичные ошибки и подводные камни
1. Гонка данных (Race Condition)
Когда два потока одновременно изменяют одну переменную. Результат непредсказуем.
// Ошибка: два потока изменяют счетчик
$counter = 0;
$runtime1 = new Runtime();
$runtime2 = new Runtime();
$runtime1->run(function() use (&$counter) {
for ($i = 0; $i < 1000; $i++) {
$counter++; // Неатомарная операция!
}
});
$runtime2->run(function() use (&$counter) {
for ($i = 0; $i < 1000; $i++) {
$counter++; // Неатомарная операция!
}
});
// Ожидаем 2000, но получаем случайное число
echo "Счетчик: $counter
";
2. Deadlock (Взаимная блокировка)
Два потока ждут друг друга, и никто не может продолжить.
// Ошибка: deadlock
$channel1 = Channel::make('ch1');
$channel2 = Channel::make('ch2');
$runtime1->run(function() use ($channel1, $channel2) {
$channel1->send("данные");
$data = $channel2->recv(); // Ждет данные из ch2
});
$runtime2->run(function() use ($channel1, $channel2) {
$channel2->send("данные");
$data = $channel1->recv(); // Ждет данные из ch1
});
// Оба потока зависли!
3. Утечка памяти в циклах событий
Если не очищать колбэки, память будет расти бесконечно.
// Ошибка: утечка памяти
$loop->addPeriodicTimer(0.1, function() use (&$largeArray) {
$largeArray[] = str_repeat('x', 1024 * 1024); // Добавляем 1 МБ каждые 0.1 сек
// Память никогда не очищается!
});
4. Игнорирование ошибок в промисах
Если не обработать ошибку в промисах, она "проглатывается".
// Ошибка: необработанная ошибка
$browser->get('https://несуществующий-сайт.xyz')
->then(function ($response) {
echo "Успех
";
});
// Ошибка будет проигнорирована, скрипт упадет
5. Блокирующие операции в асинхронном коде
Если в асинхронном коде использовать sleep() или file_get_contents(), цикл событий заблокируется.
// Ошибка: блокирующая операция
$loop->addTimer(1, function() {
sleep(5); // Блокирует весь цикл событий на 5 секунд!
echo "Таймер сработал
";
});
6. Неправильное использование глобальных переменных
В потоках глобальные переменные не разделяются (каждый поток имеет свою копию).
// Ошибка: глобальные переменные не работают в потоках
$globalVar = "тест";
$runtime->run(function() {
global $globalVar; // В потоке $globalVar не определена!
echo $globalVar;
});
Лучшие практики и советы профессионалов
1. Выбирайте правильный инструмент
- Для веб-серверов: Swoole или ReactPHP (асинхронность)
- Для CPU-интенсивных задач: parallel (многопоточность)
- Для простых скриптов: синхронный PHP (не усложняйте)
2. Используйте пулы потоков
Не создавайте поток на каждую задачу — это дорого. Используйте пул потоков.
// Пул потоков (пример с parallel)
use parallelRuntime;
use parallelFuture;
class ThreadPool {
private array $runtimes = [];
private array $futures = [];
public function __construct(private int $size = 4) {
for ($i = 0; $i < $size; $i++) {
$this->runtimes[] = new Runtime();
}
}
public function submit(callable $task, array $args = []): Future {
$runtime = array_shift($this->runtimes);
$future = $runtime->run($task, $args);
$this->futures[] = [$runtime, $future];
return $future;
}
public function waitAll(): void {
foreach ($this->futures as [$runtime, $future]) {
$future->value();
$this->runtimes[] = $runtime;
}
$this->futures = [];
}
}
3. Всегда обрабатывайте ошибки
В асинхронном коде ошибки не всплывают автоматически. Используйте ->otherwise() или try/catch в корутинах.
4. Избегайте состояния гонки
Используйте мьютексы (взаимные исключения) для критических секций.
// Использование мьютекса (parallel)
use parallelSync;
$sync = new Sync(function() {
// Критическая секция — только один поток за раз
$counter = Sync::get('counter', 0);
$counter++;
Sync::set('counter', $counter);
});
5. Тестируйте под нагрузкой
Многопоточные и асинхронные программы ведут себя по-разному под нагрузкой. Используйте инструменты вроде ab (Apache Benchmark) или wrk.
6. Документируйте архитектуру
Асинхронный код сложнее читать. Комментируйте, какие операции блокирующие, а какие нет.
7. Используйте профилировщики
Xdebug и Blackfire.io помогут найти узкие места в асинхронном коде.
Вопросы для самопроверки
-
Вопрос: В чем разница между многопоточностью и асинхронностью?
Ответ: Многопоточность — это параллельное выполнение нескольких потоков (реально или иллюзорно). Асинхронность — это неблокирующее выполнение: программа не ждет завершения операции, а переключается на другую задачу. Многопоточность лучше для CPU-интенсивных задач, асинхронность — для I/O-интенсивных.
-
Вопрос: Почему в PHP нет встроенной асинхронности, как в Python?
Ответ: PHP был создан как синхронный язык для веба. Асинхронность появилась через библиотеки (ReactPHP, Amp) и расширения (Swoole). Python изначально проектировался как универсальный язык, поэтому async/await добавили в ядро.
-
Вопрос: Что такое цикл событий (Event Loop)?
Ответ: Это бесконечный цикл, который проверяет, не произошло ли какое-то событие (пришли данные, истек таймер, завершилась операция). Если событие произошло — вызывается соответствующий колбэк. Если нет — цикл продолжает ждать.
-
Вопрос: Какая библиотека лучше для асинхронного HTTP в PHP?
Ответ: ReactPHP (библиотека react/http) — самая популярная. Amp (amphp/http-client) — альтернатива с синтаксисом корутин. Swoole — для высоконагруженных серверов.
-
Вопрос: Что такое "гонка данных" (race condition)?
Ответ: Ситуация, когда два потока одновременно обращаются к общим данным, и результат зависит от