PHP作为常用的服务端脚本语言,本身在Web请求中大多是同步阻塞执行的。但当我们需要同时抓取几十个接口、批量处理本地大文件,或者并行跑计算任务时,单进程串行往往会浪费大量等待时间。借助pthreads这类多线程扩展,PHP可以在CLI环境下开出真正的操作系统线程,把任务并行化以提升吞吐。

一、PHP多线程的运行前提
在动手写代码前,必须明确PHP多线程的边界。pthreads扩展自诞生起就要求PHP以ZTS(Zend Thread Safety,线程安全)模式编译,也就是configure时带上--enable-maintainer-zts。如果用的是常见Linux发行版自带的PHP,通常都是非线程安全版本,直接安装pthreads会报错。因此最省事的办法是使用源码重新编译,或采用集成好的线程安全环境。
另一个容易被忽略的点是:pthreads只能用在CLI(命令行)模式下。在php-fpm或apache的Web请求里创建线程,极易造成内存冲突和进程崩溃。所以下文所有示例都应在终端通过php script.php运行。此外,pthreads在PHP 7.2之后被标记为废弃,转向parallel扩展,但pthreads的思想与用法对理解PHP并发仍有价值。
二、使用pthreads实现基础多线程
最直观的方式是定义一个继承Thread类的子类,把要并行执行的逻辑写在run方法里。每个实例调用start后,PHP会向系统申请新线程并执行run;主线程可调用join等待其结束并回收资源。
下面示例创建两个工作线程,分别模拟不同耗时的任务:
<?php
class MyTask extends Thread {
private $name;
public function __construct($name) {
$this->name = $name;
}
public function run() {
// 模拟耗时操作
sleep(rand(1, 3));
echo $this->name . " 执行完毕n";
}
}
$t1 = new MyTask("线程A");
$t2 = new MyTask("线程B");
$t1->start();
$t2->start();
$t1->join();
$t2->join();
echo "全部线程结束n";
?>
上述代码在运行时,两个线程几乎同时开始,总耗时约等于较慢的那个线程而非两者之和。这就是多线程带来的并行收益。需要注意的是,echo在多线程下输出可能交错,生产环境应改用文件或消息队列收集结果。
如果主线程需要读取子线程的计算结果,可以通过在子线程中给公有属性赋值,再在join之后从原对象读取。因为pthreads对象在底层是跨线程共享的,但这种共享仅限于简单类型和实现了Threaded接口的对象。
三、数据共享与线程安全注意点
很多初学者会把普通数组或外部变量直接丢进线程,结果拿到空值或报错。pthreads规定:跨线程传递的数据必须是标量、或者继承自Threaded的类实例。普通PHP数组在线程间并不自动共享,写入子线程的数组改动也不会反映到主线程。
以下代码展示用Threaded对象做共享计数器的正确姿势:
<?php
class Counter extends Threaded {
public $count = 0;
}
class Adder extends Thread {
private $counter;
public function __construct($counter) {
$this->counter = $counter;
}
public function run() {
for ($i = 0; $i < 100; $i++) {
$this->counter->count++;
}
}
}
$c = new Counter();
$workers = [];
for ($i = 0; $i < 5; $i++) {
$w = new Adder($c);
$w->start();
$workers[] = $w;
}
foreach ($workers as $w) {
$w->join();
}
echo "最终计数: " . $c->count . "n";
?>
这里Counter继承Threaded,使其成员可在多个线程中被安全读写。但上面的例子没有加锁,高并发下count++并非原子操作,最终结果可能小于500。实际开发中应配合Threaded提供的synchronized方法或使用Mutex扩展保证原子性。
此外,线程里不能持有数据库连接、文件句柄这类资源类型的非线程安全对象。常见做法是每个线程内部自己创建连接,用完即关,避免跨线程传递资源。
四、用线程池控制资源消耗
当任务数量很大(如上千个URL),一股脑开上千个线程会拖垮机器。pthreads官方提供了Pool类,可以固定工作线程数,把任务对象提交给池子轮转执行。
示例代码如下:
<?php
class Job extends Threaded {
private $id;
public function __construct($id) {
$this->id = $id;
}
public function run() {
echo "处理任务 " . $this->id . "n";
sleep(1);
}
}
$pool = new Pool(4); // 最多4个线程
for ($i = 1; $i <= 10; $i++) {
$pool->submit(new Job($i));
}
$pool->shutdown(); // 等待所有任务完成
echo "线程池任务全部完成n";
?>
通过Pool(4)我们限制并发为4,前4个任务立即执行,其余在队列中等待空闲线程。这样既能利用多核,又不会因线程过多引发调度开销。在编写爬虫或批量处理器时,线程池是比手动管理线程更稳健的方案。
要特别注意的是,提交给Pool的任务必须是Threaded子类,普通闭包无法跨线程传递。如果任务间需要共享配置,可以把配置作为Threaded成员注入。
五、常见误区与替代方案
不少开发者误以为装了pthreads就能在Laravel或ThinkPHP的Web接口里加速响应,这是危险且无效的做法。Web模式下的PHP生命周期由FastCGI管理,强行开线程会导致段错误。如果仅是想并发请求外部API,用curl_multi_*函数做异步IO往往更简单,也无需线程安全PHP。
若环境为PHP 7.4及以上且想用现代多线程,可评估parallel扩展。它用parallelRuntime替代Thread,支持闭包并避免了部分pthreads的对象限制。但无论哪种方案,都请牢记:多线程适合CPU密集或IO等待明显的CLI任务,不要为了“看起来高级”而引入复杂度。
总结来说,PHP多线程通过pthreads在CLI下可行,核心在于理解线程安全编译、Threaded共享模型与资源限制。合理用线程池,避开Web环境,才能让它真正提升你的脚本效率。
PHP多线程pthreadsthread_safety修改时间:2026-08-07 01:18:16