信号量(Semaphore)是PHP协程并发控制中最直接、可控性最强的显式限流手段,必须手动构造、acquire()获取许可、finally中release()释放,否则会导致协程永久阻塞;go()和async本身不限流,Swoole无内置Semaphore需手写或引入第三方,pcntl多进程下内存态信号量失效,应改用Redis或消息队列协调。

PHP 本身不原生支持 async/await(直到 8.4+ 仍需扩展配合),所谓“异步任务框架”实际依赖 Swoole、ReactPHP 或 Laravel Horizon 等封装层。并发控制不是靠语言特性自动完成的,必须显式介入——信号量(Semaphore)是最直接、可控性最强的手段。
为什么不能直接用 go() 或 async 就完事
很多人以为在 Swoole 中调用 go() 启动协程就等于“自动并发控制”,其实完全相反:go() 只负责调度,不限制数量。100 个 URL 循环调用 go(),会瞬间并发 100 个 HTTP 请求,极大概率触发目标服务限流或本地 DNS/连接池耗尽。
-
go()是无节制的协程启动器,不是限流器 -
async(如 Laravel 的Bus::dispatchNow())默认走队列,但队列消费者本身若未限速,照样会压垮下游 - 信号量必须独立构造并显式
acquire()/release(),否则等于没设
在 Swoole 协程中正确使用 Semaphore
Swoole 没有内置 Semaphore 类,需手写或引入第三方(如 spiral/roadrunner 的 Concurrent\Semaphore)。最简可行实现就是知识库中那个类——但它必须和协程生命周期对齐,否则 release() 调用可能丢失。
- 构造时传入最大并发数,例如
new Semaphore(5)表示最多 5 个请求同时进行 - 每个请求前必须
await $sem->acquire(),这是挂起当前协程的阻塞点 -
release()必须在请求真正结束(包括异常路径)后调用,建议用try/finally包裹 - 不要在
onTask回调里复用同一个Semaphore实例——Task Worker 是多进程模型,Semaphore是内存态对象,跨进程无效
use Swoole\Coroutine;
class Semaphore {
private int $max;
private int $current = 0;
private array $queue = [];
public function __construct(int $max) {
$this->max = $max;
}
public function acquire(): \Swoole\Coroutine\Channel {
if ($this->current < $this->max) {
$this->current++;
return new \Swoole\Coroutine\Channel(0);
}
$ch = new \Swoole\Coroutine\Channel(0);
$this->queue[] = $ch;
return $ch;
}
public function release(): void {
$this->current--;
if (!empty($this->queue)) {
$ch = array_shift($this->queue);
$ch->push(true);
}
}
}
// 使用示例
Coroutine\run(function () {
$sem = new Semaphore(3);
$urls = ['https://httpbin.org/delay/1', 'https://httpbin.org/delay/2', 'https://httpbin.org/delay/3', 'https://httpbin.org/delay/4'];
foreach ($urls as $url) {
go(function () use ($url, $sem) {
$sem->acquire()->pop(); // 等待许可
try {
$client = new \Swoole\Coroutine\Http\Client('httpbin.org', 443, true);
$client->set(['timeout' => 5]);
$client->get(parse_url($url, PHP_URL_PATH));
echo "✅ {$url} done\n";
} finally {
$sem->release(); // 必须释放
}
});
}
});
pcntl_fork() 场景下信号量为何基本失效
多进程模型里,每个子进程拥有独立内存空间。Semaphore 实例在父进程中创建,fork() 后子进程拿到的是副本,$current 和 $queue 完全隔离。你无法靠它协调跨进程的并发数。
立即学习“PHP免费学习笔记(深入)”;
- 想在
pcntl中限流,得用系统级信号量(sem_get()+sem_acquire()),但 PHP 的sysvsem扩展在 Docker 或某些云环境常被禁用 - 更现实的做法是改用外部协调:Redis 的
INCR/EXPIRE组合模拟信号量,或直接用消息队列(如 RabbitMQ 的prefetch_count)控制消费速率 -
pcntl+ 信号量属于高维护成本方案,除非你明确需要 CPU 密集型任务隔离,否则优先选 Swoole 协程
真正难的不是写一个 Semaphore 类,而是判断哪里该加、加几层、以及释放时机是否覆盖所有异常分支。很多线上事故都源于 release() 被遗漏或放在了 catch 外面——一旦某个协程 panic,信号量永久卡死,后续所有请求排队等待,整个任务流就僵住了。



















