Пакет · thread · глубокое погружение

Вывод и правило Broken pipe

Почему start() по умолчанию задаёт outputTarget в /dev/null и что на самом деле идёт не так, когда фоновый процесс пишет в pipe, который никто не читает.

Какую поломку это предотвращает

Когда outputTarget равен null, start() направляет stdout и stderr дочернего процесса в pipe’ы обратно к родителю. У pipe фиксированный буфер ОС (~64 КБ). Если родитель его не вычитывает:

  1. Дочерний процесс продолжает echo; буфер заполняется.
  2. Следующий write() блокируется — дочерний процесс застревает и не делает работу.
  3. Если родитель завершился или закрыл свой конец чтения, вместо этого запись вызывает SIGPIPE / «Broken pipe», убивая дочерний процесс.

Для fire-and-forget задачи — а это частый случай — это тихий убийца: задача будто бы запускается, а потом застревает или умирает по причинам, никак не связанным с её логикой.

Почему /dev/null — безопасное значение по умолчанию

При значении по умолчанию outputTarget = '/dev/null' start() вообще не открывает pipe — ОС отдаёт stdout/stderr прямо в null-устройство. Ничего не буферизуется, ничего не блокируется, а родителю не нужно управлять жизненным циклом. Можно запустить задачу и спокойно выбросить объект Thread.

Компромисс

/dev/null безопасен, потому что его по определению никто не читает. Как только вы хотите читать вывод, вы осознанно переходите на pipe (null) и берёте на себя обязанность его вычитывать.

Три режима вывода

outputTarget Подключение Когда
'/dev/null' (по умолчанию) Отбрасывается ОС, без pipe Фоновые fire-and-forget задачи
'/path/to/file.log' Дописывается в файл Хранить запись; staging/продакшн
null Направляется родителю через pipe (неблокирующий) Локальная разработка — вы активно читаете

При файловой цели и stdout, и stderr открываются в режиме дописывания, поэтому переиспользовать один лог-файл между запусками безопасно. При null pipe’ы переводятся в неблокирующий режим, и вы читаете их через readOutput() / readError().

Чтение вывода из pipe

readOutput() / readError() вызывают stream_get_contents на неблокирующих pipe’ах и возвращают всё, что пришло с момента последнего вызова. Два следствия, которые стоит знать:

  • Вычитывайте в цикле, пока isAlive() — одно чтение не захватит весь вывод долгой задачи, а переполнение буфера снова приведёт к Broken pipe.
  • После join() они возвращают ''join() закрывает и очищает pipe’ы при завершении, поэтому читайте нужное до join (или в цикле, пока процесс жив).

--debug (флаг debugMode) ортогонален: он включает E_ALL + display_errors в дочернем процессе, чтобы предупреждения и notices PHP появлялись в выбранной вами цели.

Связанное