“协程可以在遇到阻塞的时候中断主动让渡资源,调度程序选择其他的协程运行。从而实现非阻塞IO”
然而php是不支持原生协程的,遇到阻塞时如不交由异步进程来执行是没有任何意义的,代码还是同步执行的,如下所示:
function foo()
{
$db=new Db();
$result=(yield $db->query());
yield $result;
}
上面的数据库查询操作是阻塞的,当调度器调度该协程到这一步时发现执行了阻塞操作,此时调度器该怎么办?选择其余协程执行?那该协程的阻塞操作又该何时执行,交由谁执行呢?所以说在php协程中抛开异步调用谈非阻塞IO属于耍流氓。
而swoole的异步task提供了一个实现异步的解决方案,关于swoole_task可以参考官方文档
核心功能实现
将一次请求形成一个协程
首先创建一个swoole_server并设置回调
class HttpServer implements Server
{
private $swooleHttpServer;
public function __construct(\swoole_http_server $swooleHttpServer)
{
$this->swooleHttpServer = $swooleHttpServer;
}
public function start()
{
$this->swooleHttpServer->on('start', [$this, 'onStart']);
$this->swooleHttpServer->on('shutdown', [$this, 'onShutdown']);
$this->swooleHttpServer->on('workerStart', [$this, 'onWorkerStart']);
$this->swooleHttpServer->on('workerStop', [$this, 'onWorkerStop']);
$this->swooleHttpServer->on('workerError', [$this, 'onWorkerError']);
$this->swooleHttpServer->on('task', [$this, 'onTask']);
$this->swooleHttpServer->on('finish', [$this, 'onFinish']);
$this->swooleHttpServer->on('request', [$this, 'onRequest']);
$this->swooleHttpServer->start();
}
onRequest方法:
public function onRequest(\swoole_http_request $request, \swoole_http_response $response)
{
$requestHandler = new RequestHandler($request, $response);
$requestHandler->handle();
}
在ReqeustHandler中执行handle方法,来解析请求的路由,并创建控制器,调用相应的方法,相
public function handle()
{
$this->context = new Context($this->request, $this->response, $this->getFd());
$this->router = new Router($this->request);
try {
if (false === $this->router->parse()) {
$this->response->output('');
return;
}
$coroutine = $this->doRun();
$task = new Task($coroutine, $this->context);
$task->run();
} catch (\Exception $e) {
PcsExceptionHandler::handle($e, $this->response);
}
}
private function doRun()
{
$ret = (yield $this->dispatch());
yield $this->response->send($ret);
}
上面代码中的ret是action()的调用结果,yield $this->response->send($ret);是向对客户端请求的应答。
$coroutine是这一次请求形成的一个协程(Genetator对象),包含了整个请求的流程,接下来就要对这个协程进行调度来获取真正的执行结果。
协程调度
namespace Pcs\Coroutine;
use Pcs\Network\Context\Context;
class Task
{
private $coroutine;
private $context;
private $status;
private $scheduler;
private $sendValue;
public function __construct(\Generator $coroutine, Context $context)
{
$this->coroutine = $coroutine;
$this->context = $context;
$this->scheduler = new Scheduler($this);
}
public function run()
{
while (true) {
try {
$this->status = $this->scheduler->schedule();
switch ($this->status) {
case TaskStatus::TASK_WAIT:
echo "task status: TASK_WAIT\n";
return null;
case TaskStatus::TASK_DONE:
echo "task status: TASK_DONE\n";
return null;
case TaskStatus::TASK_CONTINUE;
echo "task status: TASK_CONTINUE\n";
break;
}
} catch (\Exception $e) {
$this->scheduler->throwException($e);
}
}
}
public function setCoroutine($coroutine)
{
$this->coroutine = $coroutine;
}
public function getCoroutine()
{
return $this->coroutine;
}
public function valid()
{
if ($this->coroutine->valid()) {
return true;
} else {
return false;
}
}
public function send($value)
{
$this->sendValue = $value;
$ret = $this->coroutine->send($value);
return $ret;
}
public function getSendVal()
{
return $this->sendValue;
}
}
Task依赖于Generator对象$coroutine,在Task类中定义了一些get/set方法,以及一些Generator的方法,Task::run()方法用来执行对协程的调度,调度行为由Schedule来执行,每次调度都会返回当前这次调度的状态。多个协程共用一个调度器,而这里run方法会为每个协程创建一个调度器,原因是每个协程都是一个客户端的请求,使用一个单独的调度器能减少相互间的影响,而且多个协程之间的调度顺序是swoole来处理的,这里的调度器不用关心。下面给出调度的代码:
namespace Pcs\Coroutine;
class Scheduler
{
private $task;
private $stack;
const SCHEDULE_CONTINUE = 10;
public function __construct(Task $task)
{
$this->task = $task;
$this->stack = new \SplStack();
}
public function schedule()
{
$coroutine = $this->task->getCoroutine();
$value = $coroutine->current();
$status = $this->handleSystemCall($value);
if ($status !== self::SCHEDULE_CONTINUE) return $status;
$status = $this->handleStackPush($value);
if ($status !== self::SCHEDULE_CONTINUE) return $status;
$status = $this->handleAsyncJob($value);
if ($status !== self::SCHEDULE_CONTINUE) return $status;
$status = $this->handelYieldValue($value);
if ($status !== self::SCHEDULE_CONTINUE) return $status;
$status = $this->handelStackPop();
if ($status !== self::SCHEDULE_CONTINUE) return $status;
return TaskStatus::TASK_DONE;
}
public function isStackEmpty()
{
return $this->stack->isEmpty();
}
private function handleSystemCall($value)
{
if (!$value instanceof SystemCall) {
return self::SCHEDULE_CONTINUE;
}
}
private function handleStackPush($value)
{
if (!$value instanceof \Generator) {
return self::SCHEDULE_CONTINUE;
}
$coroutine = $this->task->getCoroutine();
$this->stack->push($coroutine);
$this->task->setCoroutine($value);
return TaskStatus::TASK_CONTINUE;
}
private function handleAsyncJob($value)
{
if (!is_subclass_of($value, Async::class)) {
return self::SCHEDULE_CONTINUE;
}
$value->execute([$this, 'asyncCallback']);
return TaskStatus::TASK_WAIT;
}
public function asyncCallback($response, $exception = null)
{
if ($exception !== null
&& $exception instanceof \Exception
) {
$this->throwException($exception, true);
} else {
$this->task->send($response);
$this->task->run();
}
}
private function handelYieldValue($value)
{
if (!$this->task->valid()) {
return self::SCHEDULE_CONTINUE;
}
$ret = $this->task->send($value);
return TaskStatus::TASK_CONTINUE;
}
private function handelStackPop()
{
if ($this->isStackEmpty()) {
return self::SCHEDULE_CONTINUE;
}
$coroutine = $this->stack->pop();
$this->task->setCoroutine($coroutine);
$value = $this->task->getSendVal();
$this->task->send($value);
return TaskStatus::TASK_CONTINUE;
}
public function throwException($e, $isFirstCall = false)
{
if ($this->isStackEmpty()) {
$this->task->getCoroutine()->throw($e);
return;
}
try {
if ($isFirstCall) {
$coroutine = $this->task->getCoroutine();
} else {
$coroutine = $this->stack->pop();
}
$this->task->setCoroutine($coroutine);
$coroutine->throw($e);
$this->task->run();
} catch (\Exception $e) {
$this->throwException($e);
}
}
}
Scheduler中的schedule方法会获取当前Task的协程,并通过current()方法获取当前中断点的返回值,接着依次调用5个方法来对返回值进行处理。
1:handleSystemCall
如果返回的值是SystemCall类型的对象,则执行系统调用,如killTask之类的操作,systemCall是第一优先级。
2:handleStackPush
在A函数中调用B函数,则B函数称为A函数的子例程(子函数),然而在协程中却不能像普通函数那样调用。
function funcA()
{
return funcB();
}
function genA()
{
yield genB();
}
在funcA中funcB();会返回funcB的执行结果,但是在genA中,yield genB();会返回一个Generator对象,而不是genB的最终执行结果。想得到genB的执行结果需要对genB进行调度,而genB中又可能有genC()genD()的协程嵌套,所以为了让协程像函数一眼正常调用,这里使用协程栈来实现。
如上图,当调度器获取到GenA(父协程)的返回值is instance of Generator时,调度器会把父协程push到stack中,然后把子协程分配给Task,继续调度子协程。如此反复直到最后一个子协程返回,然后开始pop,将stack中的协程依次取出
3:handleAsyncJob
handleAsyncJob是整个协程调度的核心
private function handleAsyncJob($value)
{
if (!is_subclass_of($value, Async::class)) {
return self::SCHEDULE_CONTINUE;
}
$value->execute([$this, 'asyncCallback']);
return TaskStatus::TASK_WAIT;
}
public function asyncCallback($response, $exception = null)
{
if ($exception !== null
&& $exception instanceof \Exception
) {
$this->throwException($exception, true);
} else {
$this->task->send($response);
$this->task->run();
}
}
当协程调度的返回值是继承了Async的子类或者是实现了Asycn接口的实例的时候,会执行Async的execute方法。这里用mysqli数据库查询类举例。
public function execute(callable $callback)
{
$this->callback = $callback;
$serv = ServerHolder::getServer();
$serv->task($this->sql, -1, [$this, 'queryReady']);
}
public function queryReady(\swoole_http_server $serv, $task_id, $data)
{
$queryResult = unserialize($data);
$exception = null;
if ($queryResult->errno != 0) {
$exception = new \Exception($queryResult->error);
}
call_user_func_array($this->callback, [$queryResult, $exception]);
}
execute方法接收一个函数作为该异步操作完成之后的回调函数,在Mysqli类中的execute方法中,启动了一个异步swoole_task,将sql操作交给swoole_task异步执行,在执行结束后会执行queryReady方法,该方法在解析异步返回数据之后执行$this->callback()也就是之前在调度器中传入的 asyncCallback方法,该方法在检测异常之后会执行send()方法将异步执行的结果发送到中断处,继续执行。
handleAsyncJob不会等待异步操作的返回结果,而是直接返回TASK_WAIT信号,回到上面的Task->run()方法可以看到TASK_WAIT信号会导致run()方法返回null,释放当前worker,调度流程图如下图所示,
4:handleYieldValue
private function handelYieldValue($value)
{
if (!$this->task->valid()) {
return self::SCHEDULE_CONTINUE;
}
$ret = $this->task->send($value);
return TaskStatus::TASK_CONTINUE;
}
如果某次yield的返回值既不是异步调用也不是Generator,那么判断当前的generator是否是valid(是否执行完)如果执行完毕,继续调度,执行下面的handleStackPush方法,否则的话返回Task_Continue继续调度,也就是说在一个generator中多次yield,最后只会取最后一次yield的返回值。
5:handleStackPush
当上一步中判断!$this->task->valid()也就是当前生成器执行完毕的时候,会执行本方法来控制之前的协程stack进行pop操作,首先检查Stac是否是非空,非空的话pop出一个父协程,并将当前协程的返回值send()到父协程中断出继续执行。
协程优势在哪里
当一次请求遇到IO的时候,同步操作会导致当前请求阻塞在IO处等待IO返回,体现在swoole上就是一个请求一直占用一个worker。
但是当使用了协程调度之后,用户可以在阻塞的地方通过yield手动中断,交由swoole_task去异步操作,同时释放worker占用来处理其他请求。
当异步处理执行结束后再继续调度。
注意 php的协程只负责中断,异步操作是Swoole_task做的
- 并发编程:协程TCP、非阻塞IO、多路复用、
一.线程池实现阻塞IO 二.非阻塞IO模型 三.多路复用,降低CPU占用 四.模拟异步IO 一.线程池实现阻塞IO 线程阻塞IO 客户端 import socket c = socket.socket ...
- Python异步非阻塞IO多路复用Select/Poll/Epoll使用,线程,进程,协程
1.使用select模拟socketserver伪并发处理客户端请求,代码如下: import socket import select sk = socket.socket() sk.bind((' ...
- 异步非阻塞IO的Python Web框架--Tornado
Tornado的全称是Torado Web Server,从名字上就可知它可用作Web服务器,但同时它也是一个Python Web的开发框架.最初是在FriendFeed公司的网站上使用,FaceBo ...
- 转一贴,今天实在写累了,也看累了--【Python异步非阻塞IO多路复用Select/Poll/Epoll使用】
下面这篇,原理理解了, 再结合 这一周来的心得体会,整个框架就差不多了... http://www.haiyun.me/archives/1056.html 有许多封装好的异步非阻塞IO多路复用框架, ...
- nodejs的异步非阻塞IO
简单表述一下:发启向系统IO操作请求,系统使用线程池IO操作,执行完放到事件队列里,node主线程轮询事件队列,读取结果与调用回调.所以说node并非真的单线程,还是使用了线程池的多线程. 上个图看看 ...
- suging闲谈-netty 的异步非阻塞IO线程与业务线程分离
前言 surging 对外沉寂了一段时间了,但是作者并没有闲着,而是针对于客户的需要添加了不少功能,也给我带来了不少外快收益, 就比如协议转化,consul 的watcher 机制,JAVA版本,sk ...
- [Flask] 异步非阻塞IO实现
Flask默认是不支持非阻塞IO的,表现为: 当 请求1未完成之前,请求2是需要等待处理状态,效率非常低. 在flask中非阻塞实现可以由2种: 启用flask多线程机制 # Flask from f ...
- Python3的原生协程(Async/Await)和Tornado异步非阻塞
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_113 我们知道在程序在执行 IO 密集型任务的时候,程序会因为等待 IO 而阻塞,而协程作为一种用户态的轻量级线程,可以帮我们解决 ...
- 谈谈对不同I/O模型的理解 (阻塞/非阻塞IO,同步/异步IO)
一.关于I/O模型的问题 最近通过对ucore操作系统的学习,让我打开了操作系统内核这一黑盒子,与之前所学知识结合起来,解答了长久以来困扰我的关于I/O的一些问题. 1. 为什么redis能以单工作线 ...
随机推荐
- Java如何创建不存在的指定路径的文件?
实际应用中,要在指定位置创建一个文件,但文件及文件之前的目录都不存在,此时可用以下方法进行创建. 以下是主要代码: File f = new File("I:" + File.se ...
- Git 从入门到熟练|不敢说精通
前言 如果有一定版本管理软件基础或使用过 svn 的你,这篇 git 的文章应该是最适合你的.作者也是从 svn 过来,从开始的觉得 git 麻烦,到最后还是感觉 git 是最好用的版本控制软件. 虽 ...
- preg_relace_callback不起作用匿名函数不启作用替换字符串中的所有图片
遇到这样的一个需求,即替换新闻正文中的所有图片,将其图片地址补充为完整的地址. 刚开始的时候,采用匿名函数的方法可以使用,但有一个问题,好像是php的匿名函数5.4以前的版本支持的并不好. 然后在内部 ...
- Spring入门教程
Spring新手入门教程,配套下面这两个大神的课程就可以了. 一个是Spring视频教程. 一个是Spring博客教程. https://www.imooc.com/learn/196 http:// ...
- Mysql高手系列 - 第13篇:细说NULL导致的神坑,让人防不胜防
这是Mysql系列第13篇. 环境:mysql5.7.25,cmd命令中进行演示. 当数据的值为NULL的时候,可能出现各种意想不到的效果,让人防不胜防,我们来看看NULL导致的各种神坑,如何避免? ...
- 网关鉴权后下游统一filter获取用户信息
1. 场景描述 最近有点忙,在弄微服务nacos+springcloud gateway这块工作,以前只是简单应用,这次因为要对接10几个系统或者平台,还的鉴权,等后续稍微闲点了,把这块东西总结下. ...
- CentOS 7上编写自定义系统审计规则
1)简介 Linux审计系统创建审计跟踪,这是一种跟踪系统上各种信息的方法.它可以记录大量数据,如事件类型,日期和时间,用户ID,系统调用,进程,使用的文件,SELinux上下文和敏感度级别.它可以跟 ...
- .Net Reactor混淆导致匿名类处理出现的问题处理分析
.Net Reactor 是一款比较不错的混淆工具,比VS自带的那个好用很多,一直以来也陪伴着我们的成长,虽然没有完美的混淆工具,不过也算还是不错的,至少能在一定程度上对DLL进行一定的保护处理. 不 ...
- java-不定项参数(可变参数)的作用和使用方式
引言: 我们在编写方法的过程中,可能会遇见一个方法有不确定参数个数的情况.一般我们会用方法重载来解决问题: 1 //方法重载,解决参数个数不确定问题 2 public void method(); 3 ...
- redis分布式锁-基本概念与实现方式对比
1.redis中使用WATCH实现锁机制,是最次之的方式.WATCH只会在数据被其他客户端抢先修改了的情况下,“通知”执行了这个命令的客户端,而不会阻止其他客户端对数据进行修改.此类锁成为“乐观锁” ...