Модель работы
Одна мысль объясняет почти каждое правило этой библиотеки: «thread» здесь — это отдельный процесс операционной системы, а не поток в памяти. Держите её в голове — остальное вытекает само.
Не тот thread, что вы могли ожидать
Во многих языках поток разделяет память с кодом, который его запустил. Winter Thread так
не делает. Каждый запущенный Thread — это отдельный процесс PHP, порождённый ОС,
со своей памятью, своими файловыми дескрипторами, своим всем.
Представляйте это не как второго сотрудника за вашим столом, который смотрит в ваши записи, а как запечатанный конверт, отправленный коллеге в другом здании: он вскрывает его, делает работу и присылает результат обратно. Вашего стола он не касается.
Это осознанный компромисс:
- Изоляция — фатальная ошибка, утечка памяти или segfault в дочернем процессе не могут повредить или уронить ваш основной скрипт или его «соседей».
- Настоящий параллелизм — ОС может распределить каждый процесс на отдельное ядро CPU.
- Взамен — нет общей памяти, поэтому нельзя просто прочитать переменную, которую изменил дочерний процесс.
Граница сериализации
Вот часть, которую стоит усвоить. Когда вы запускаете задачу, объект Runnable
сериализуется (замораживается в строку), передаётся в новый процесс и
десериализуется (размораживается) на другой стороне. Только после этого выполняется
run().
Родительский процесс Дочерний процесс
-------------------- ----------------
new ReportGenerator(42)
│ сериализация
▼
"...замороженные байты..." ────► десериализация → ReportGenerator(42)
│
▼
run()Отсюда следуют два вывода, которые объясняют правила, встречающиеся повсюду:
- Никаких живых ресурсов в свойствах. Соединение с БД, открытый файл или сокет нельзя
заморозить и передать. Оставьте в конструкторе только простые данные (id, пути, флаги),
а соединения открывайте внутри
run(). - Изменения в дочернем процессе остаются в нём. Всё, что
run()меняет в$this— или в памяти — исчезает при завершении процесса. Чтобы вернуть результат, пишите в файл или базу данных либо сигнализируйте об успехе через код выхода.
Вот почему
Каждое замечание «создавайте ресурсы внутри run()» и «свойства должны быть сериализуемыми» в этой документации восходит к этой единственной границе. Это не причуда — это цена изоляции.
Что происходит при вызове start()
Спокойно, от начала до конца:
- Ваш родительский скрипт сериализует
Runnable. - Он запускает новый процесс PHP с внутренним runner-скриптом.
start()сразу возвращает PID дочернего процесса — ваш скрипт не блокируется.- Runner десериализует задачу и вызывает
run(). - Когда вы вызываете
join(), родитель ждёт завершения этого процесса и читает его код выхода.
Это весь жизненный цикл. Детальная версия — дескрипторы, внутренности runner, как устроен вывод — в Глубокое погружение → Жизненный цикл runner.
Когда стоит применять
- ✅ Вынос медленных задач (кодирование, отчёты, массовые рассылки), чтобы запрос оставался отзывчивым.
- ✅ Одновременный запуск нескольких независимых задач на разных ядрах.
- ❌ Задачи, которым нужно постоянно разделять и менять одно и то же состояние в памяти — процессы не делят память; координируйтесь через файлы, базу данных или очередь.
Дальше
- Попробовать на практике: Быстрый старт
- Рецепты задач: Обработчик очереди · Параллельные задачи
- Полный справочник: Справочник API