从定义上来说,协程是一种轻量级的线程,由用户代码来调度和管理,而不是由操作系统内核来进行调度,也就是在用户态进行。可以直接的理解为就是一个非标准的线程实现,但什么时候切换由用户自己来实现,而不是由操作系统分配
CPU
时间决定。具体来说,Swoole
的每个Worker 进程
会存在一个协程调度器来调度协程,协程切换的时机就是遇到I/O
操作或代码显性切换时,进程内以单线程的形式运行协程,也就意味着一个进程内同一时间只会有一个协程在运行且切换时机明确,也就无需处理像多线程编程下的各种同步锁的问题。
我们知道,操作系统调度的最小单位是线程,所以对于所谓的协程
来说,操作系统压根管不到。但是协程是运行在单线程里面的,只有当线程在获得cpu
执行时间片的时候,协程才能执行。但是哪个协程来执行呢?这个就涉及到协程调度算法了。
举个http
服务器的onRequest
栗子:
// pseudo code
run(function() {
for (;;) {
$conn = $listener->accept(); // 接受请求,这里会阻塞
go(function() use($conn) { // 使用协程去执行
$request = new Swoole\Request(fread($conn, 64<<10)); // 读取消息并解析http协议为request对象
$response = new Swoole\Response(); // swoole的http响应对象
(new Hyperf\HttpServer\Server())->onRequest($request, $response); //执行绑定的方法
}
}
});
在一个进程单线程内,如果想要并发执行多个coroutine
协程,也就来回切换多个go(function)
执行, 那么就需要每个协程让出时间片让别的协程获得执行。如果某个协程不让出,独占时间片执行,那么就是一个传统的同步阻塞循环逻辑。显然,这不是我们想要的,我们想要的是能够在IO操作的时候让出时间片让别的协程继续执行,从而达到高并发处理效果。
因为swoole的协程模型是单线程的,所以绝对不能让协程里面的io操作是同步阻塞的。swoole花了很多时间来协程化这些操作,包括各种客户端的异步化。因为一旦同步阻塞,那么设计出来的协程是没有意义的,就像上面说的,退化成了同步阻塞,而且有性能损耗。
Swoole
的协程还有并发问题吗? 当然,只要是支持并发的设计,就逃不开并发问题。
举个错误的栗子:
run(static function(){
$resource = [];
$i = 2;
while($i>0) {
go(function() use(&$resource){
$resource['a'] = mt_rand();
echo \Hyperf\Utils\Coroutine::id() . ':' . $resource['a'].PHP_EOL;
sleep(1); // 协程会在这里被调度切换
echo \Hyperf\Utils\Coroutine::id() . ':' . $resource['a'].PHP_EOL;
});
$i--;
}
});
// output
2:458436354
3:1088788095
2:1088788095
3:1088788095
可以很奇怪的看出,协程id(2)
的前后2个值不一样,这就是并发带来的问题。
在一个进程内同一时间只会有一个协程在运行,这个是swoole设计的特点,但是为什么要这样子设计呢?文档上面简单的说明了一句可以无需处理像多线程编程下的各种同步锁问题
。
如果是协程是多线程的,我们可以想想,每个协程在读写公共资源的时候,如果资源本身不是并发安全的,那么访问资源会出现未知的行为。这是不安全的,所以需要对公共资源加上各种同步锁来隔离并发访问。这样子的设计,显然让PHP变成了另外一种语言了。所以swoole为了解决phper自身没有这种并发编程经验,设计了这样子的一个协程模型。
虽然并发访问资源的问题避免了,但是逻辑上的并发却没有,就像上面的那个例子。协程在被切换调度以后,是需要实时注意是否资源是否被修改了。swoole建议不要使用全局资源来回避这个问题。实际上是可以用,这就需要使用者保持清醒的头脑里,因为它还有可能带来内存泄露等等问题。
swoole的协程是单线程的并且可以解决很多在多线程下的同步锁问题,但这也已经让绝大部分PHP类库不可用了。虽然,swoole官方有在做大量的协程化和客户端异步化,但是这力量是远远不够的。
所以,我想为什么swoole不直接做成多线程模型呢?多线程模型可以不用花大量的时间做异步化,只需要关注并发同步问题。这样子何乐而不为呢?或许是有php情结在里面吧。