协程中应使用 Swoole\Coroutine\Lock 而非 Swoole\Mutex,因后者基于 pthread_mutex_t,会阻塞整个 OS 线程且不协程安全;文件锁适用于进程间同步,Redis 锁用于分布式场景,且需注意锁的正确释放与生命周期管理。

协程里不能用线程锁,Swoole 没有“线程锁”这回事——它只有协程级锁和进程级锁,混用会直接失效或阻塞住。
为什么 Swoole\Mutex 在协程中不推荐用
很多人看到 Swoole\Mutex 就以为是“多线程锁”,其实它是基于系统 pthread_mutex_t 的进程/线程级互斥锁。在 Swoole 协程环境中:
• 它会阻塞整个协程所在的 OS 线程,不是只挂起当前协程
• 多个协程竞争同一个 Mutex 时,极易造成线程争抢、调度混乱,甚至死锁
• 官方已明确标注:该类不适用于协程上下文(PECL 文档中注明 “not coroutine-safe”)
• 实测中,lock() 调用可能让整个 Worker 进程卡住,监控显示 CPU 无负载但请求无响应
Swoole\Coroutine\Lock 才是协程安全的正确选择
这是专为协程设计的轻量级锁,底层用的是协程调度器的唤醒机制,不会抢占 OS 线程:
• lock() 是协程挂起等待,不阻塞线程
• trylock() 立即返回布尔值,适合做非阻塞重试逻辑
• 支持嵌套调用(可重入),同一个协程重复 lock() 不会死锁
• 锁对象本身不跨协程共享,如需多协程共用,必须通过全局变量或依赖注入传递同一实例
use Swoole\Coroutine\Lock;
$lock = new Lock();
go(function () use ($lock) {
$lock->lock();
echo "协程 A 拿到锁\n";
co::sleep(0.1);
$lock->unlock();
});
go(function () use ($lock) {
if ($lock->trylock()) {
echo "协程 B 快速拿到锁\n";
$lock->unlock();
} else {
echo "协程 B 没抢到,跳过\n";
}
});
文件锁 Swoole\Lock::FILELOCK 适用哪些场景
它本质是 PHP flock() 的封装,靠文件 inode 实现跨进程同步,和协程无关,但能“曲线救国”解决某些分布式临界区问题:
• 适合 Worker 进程间(非协程间)的简单同步,比如日志轮转、定时任务防重跑
• 锁路径必须是所有进程可见的同一文件(如 /tmp/myapp.lock),虚拟路径无效
• lock() 会阻塞当前进程,不能在协程里直接调用,否则拖垮整个协程调度
• 更安全的做法:用 trylock() + 退避重试,在协程中可控地尝试获取
$fileLock = new Swoole\Lock(Swoole\Lock::FILELOCK, '/tmp/cron.lock');
if ($fileLock->trylock()) {
// 执行单例任务
do_something_once();
$fileLock->unlock();
} else {
echo "已有其他进程在执行,跳过\n";
}
Redis 分布式锁不是“替代品”,而是另一层需求
协程锁和文件锁都局限在单机内;一旦涉及多机器部署、Worker 进程重启、或需要超时自动释放,就必须上 Redis:
• 单靠 set(['NX', 'EX' => 10]) 不够,必须用唯一 value(如 uniqid())防止误删
• 释放锁必须用 Lua 脚本保证原子性:if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
• 不要用 co::sleep() 做重试轮询,高并发下会压垮 Redis;建议用指数退避 + 最大重试次数
• 注意 Redis 连接必须是协程版(Swoole\Coroutine\Redis),普通 Redis 类会阻塞线程
真正容易被忽略的点是:锁的生命周期必须和业务逻辑严格对齐。比如在协程里加了 Lock,但中间抛了异常没 unlock(),这个锁就永远卡住了——所以务必用 try ... finally 包裹,或者封装成 defer 风格的清理逻辑。










