这篇文章给大家分享的内容是关于Swoft 源码剖析之Swoole和Swoft的一些介绍(Task投递/定时任务篇),有一定的参考价值,有需要的朋友可以参考一下。

前言

Swoft的任务功能基于SwooleTask机制,或者说SwoftTask机制本质就是对SwooleTask机制的封装和加强。

任务投递

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

//Swoft\Task\Task.php

class Task

{

    /**

     * Deliver coroutine or async task

     *

     * @param string $taskName

     * @param string $methodName

     * @param array  $params

     * @param string $type

     * @param int    $timeout

     *

     * @return bool|array

     * @throws TaskException

     */

    public static function deliver(string $taskName, string $methodName, array $params = [], string $type = self::TYPE_CO, $timeout = 3)

    {

        $data   = TaskHelper::pack($taskName, $methodName, $params, $type);

        if(!App::isWorkerStatus() && !App::isCoContext()){

            return self::deliverByQueue($data);//见下文Command章节

        }

        if(!App::isWorkerStatus() && App::isCoContext()){

            throw new TaskException('Please deliver task by http!');

        }

        $server = App::$server->getServer();

        // Delier coroutine task

        if ($type == self::TYPE_CO) {

            $tasks[0]  = $data;

            $prifleKey = 'task' . '.' . $taskName . '.' . $methodName;

            App::profileStart($prifleKey);

            $result = $server->taskCo($tasks, $timeout);

            App::profileEnd($prifleKey);

            return $result;

        }

        // Deliver async task

        return $server->task($data);

    }

}

任务投递Task::deliver()将调用参数打包后根据$type参数通过Swoole$server->taskCo()$server->task()接口投递到Task进程
Task本身始终是同步执行的,$type仅仅影响投递这一操作的行为,Task::TYPE_ASYNC对应的$server->task()是异步投递,Task::deliver()调用后马上返回;Task::TYPE_CO对应的$server->taskCo()是协程投递,投递后让出协程控制,任务完成或执行超时后Task::deliver()才从协程返回。

任务执行

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

//Swoft\Task\Bootstrap\Listeners\TaskEventListener

/**

 * The listener of swoole task

 * @SwooleListener({

 *     SwooleEvent::ON_TASK,

 *     SwooleEvent::ON_FINISH,

 * })

 */

class TaskEventListener implements TaskInterface, FinishInterface

{

    /**

     * @param \Swoole\Server $server

     * @param int            $taskId

     * @param int            $workerId

     * @param mixed          $data

     * @return mixed

     * @throws \InvalidArgumentException

     */

    public function onTask(Server $server, int $taskId, int $workerId, $data)

    {

        try {

            /* @var TaskExecutor $taskExecutor*/

            $taskExecutor = App::getBean(TaskExecutor::class);

            $result = $taskExecutor->run($data);

        } catch (\Throwable $throwable) {

            App::error(sprintf('TaskExecutor->run %s file=%s line=%d ', $throwable->getMessage(), $throwable->getFile(), $throwable->getLine()));

            $result = false;

            // Release system resources

            App::trigger(AppEvent::RESOURCE_RELEASE);

            App::trigger(TaskEvent::AFTER_TASK);

        }

        return $result;

    }

}

此处是swoole.onTask的事件回调,其职责仅仅是将将Worker进程投递来的打包后的数据转发给TaskExecutor

SwooleTask机制的本质是Worker进程将耗时任务投递给同步的Task进程(又名TaskWorker)处理,所以swoole.onTask的事件回调是在Task进程中执行的。上文说过,Worker进程是你大部分HTTP服务代码执行的环境,但是从TaskEventListener.onTask()方法开始,代码的执行环境都是Task进程,也就是说,TaskExecutor和具体的TaskBean都是执行在Task进程中的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

//Swoft\Task\TaskExecutor

/**

 * The task executor

 *

 * @Bean()

 */

class TaskExecutor

{

    /**

     * @param string $data

     * @return mixed

    */

    public function run(string $data)

    {

        $data = TaskHelper::unpack($data);

        $name   = $data['name'];

        $type   = $data['type'];

        $method = $data['method'];

        $params = $data['params'];

        $logid  = $data['logid'] ?? uniqid('', true);

        $spanid = $data['spanid'] ?? 0;

        $collector = TaskCollector::getCollector();

        if (!isset($collector['task'][$name])) {

            return false;

        }

        list(, $coroutine) = $collector['task'][$name];

        $task = App::getBean($name);

        if ($coroutine) {

            $result = $this->runCoTask($task, $method, $params, $logid, $spanid, $name, $type);

        } else {

            $result = $this->runSyncTask($task, $method, $params, $logid, $spanid, $name, $type);

        }

        return $result;

    }

}

任务执行思路很简单,将Worker进程发过来的数据解包还原成原来的调用参数,根据$name参数找到对应的TaskBean并调用其对应的task()方法。其中TaskBean使用类级别注解@Task(name="TaskName")或者@Task("TaskName")声明。

值得一提的一点是,@Task注解除了name属性,还有一个coroutine属性,上述代码会根据该参数选择使用协程的runCoTask()或者同步的runSyncTask()执行Task。但是由于而且由于SwooleTask进程的执行是完全同步的,不支持协程,所以目前版本请该参数不要配置为true。同样的在TaskBean中编写的任务代码必须的同步阻塞的或者是要能根据环境自动将异步非阻塞和协程降级为同步阻塞的

从Process中投递任务

前面我们提到:

SwooleTask机制的本质是Worker进程将耗时任务投递给同步的Task进程(又名 TaskWorker)处理。

换句话说,Swoole$server->taskCo()$server->task()都只能在Worker进程中使用。
这个限制大大的限制了使用场景。 如何能够为了能够在Process中投递任务呢?Swoft为了绕过这个限制提供了Task::deliverByProcess()方法。其实现原理也很简单,通过Swoole$server->sendMessage()方法将调用信息从Process中投递到Worker进程中,然后由Worker进程替其投递到Task进程当中,相关代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

//Swoft\Task\Task.php

/**

 * Deliver task by process

 *

 * @param string $taskName

 * @param string $methodName

 * @param array  $params

 * @param string $type

 * @param int    $timeout

 * @param int    $workId

 *

 * @return bool

 */

public static function deliverByProcess(string $taskName, string $methodName, array $params = [], int $timeout = 3, int $workId = 0, string $type = self::TYPE_ASYNC): bool

{

    /* @var PipeMessageInterface $pipeMessage */

    $server      = App::$server->getServer();

    $pipeMessage = App::getBean(PipeMessage::class);

    $data = [

        'name'    => $taskName,

        'method'  => $methodName,

        'params'  => $params,

        'timeout' => $timeout,

        'type'    => $type,

    ];

    $message = $pipeMessage->pack(PipeMessage::MESSAGE_TYPE_TASK, $data);

    return $server->sendMessage($message, $workId);

}

数据打包后使用$server->sendMessage()投递给Worker:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

//Swoft\Bootstrap\Server\ServerTrait.php

/**

 * onPipeMessage event callback

 *

 * @param \Swoole\Server $server

 * @param int            $srcWorkerId

 * @param string         $message

 * @return void

 * @throws \InvalidArgumentException

 */

public function onPipeMessage(Server $server, int $srcWorkerId, string $message)

{

    /* @var PipeMessageInterface $pipeMessage */

    $pipeMessage = App::getBean(PipeMessage::class);

    list($type, $data) = $pipeMessage->unpack($message);

    App::trigger(AppEvent::PIPE_MESSAGE, null, $type, $data, $srcWorkerId);

}

$server->sendMessage后,Worker进程收到数据时会触发一个swoole.pipeMessage事件的回调,Swoft会将其转换成自己的swoft.pipeMessage事件并触发.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

//Swoft\Task\Event\Listeners\PipeMessageListener.php

/**

 * The pipe message listener

 *

 * @Listener(event=AppEvent::PIPE_MESSAGE)

 */

class PipeMessageListener implements EventHandlerInterface

{

    /**

     * @param \Swoft\Event\EventInterface $event

     */

    public function handle(EventInterface $event)

    {

        $params = $event->getParams();

        if (count($params) < 3) {

            return;

        }

        list($type, $data, $srcWorkerId) = $params;

        if ($type != PipeMessage::MESSAGE_TYPE_TASK) {

            return;

        }

        $type       = $data['type'];

        $taskName   = $data['name'];

        $params     = $data['params'];

        $timeout    = $data['timeout'];

        $methodName = $data['method'];

        // delever task

        Task::deliver($taskName, $methodName, $params, $type, $timeout);

    }

}

swoft.pipeMessage事件最终由PipeMessageListener处理。在相关的监听其中,如果发现swoft.pipeMessage事件由Task::deliverByProcess()产生的,Worker进程会替其执行一次Task::deliver(),最终将任务数据投递到TaskWorker进程中。

一道简单的回顾练习:从Task::deliverByProcess()到某TaskBean 最终执行任务,经历了哪些进程,而调用链的哪些部分又分别是在哪些进程中执行?

从Command进程或其子进程中投递任务

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

//Swoft\Task\QueueTask.php

/**

 * @param string $data

 * @param int    $taskWorkerId

 * @param int    $srcWorkerId

 *

 * @return bool

 */

public function deliver(string $data, int $taskWorkerId = null, $srcWorkerId = null)

{

    if ($taskWorkerId === null) {

        $taskWorkerId = mt_rand($this->workerNum + 1, $this->workerNum + $this->taskNum);

    }

    if ($srcWorkerId === null) {

        $srcWorkerId = mt_rand(0, $this->workerNum - 1);

    }

    $this->check();

    $data   = $this->pack($data, $srcWorkerId);

    $result = \msg_send($this->queueId, $taskWorkerId, $data, false);

    if (!$result) {

        return false;

    }

    return true;

}

对于Command进程的任务投递,情况会更复杂一点。
上文提到的Process,其往往衍生于Http/Rpc服务,作为同一个Manager的子孙进程,他们能够拿到Swoole\Server的句柄变量,从而通过$server->sendMessage(),$server->task()等方法进行任务投递。

但在Swoft的体系中,还有一个十分路人的角色: Command
Command的进程从shellcronb独立启动,和Http/Rpc服务相关的进程没有亲缘关系。因此Command进程以及从Command中启动的Process进程是没有办法拿到Swoole\Server的调用句柄直接通过UnixSocket进行任务投递的。
为了为这种进程提供任务投递支持,Swoft利用了SwooleTask进程的一个特殊功能----消息队列。

同一个项目中CommandHttp\RpcServer 通过约定一个message_queue_key获取到系统内核中的同一条消息队列,然后Comand进程就可以通过该消息队列向Task进程投递任务了。
该机制没有提供对外的公开方法,仅仅被包含在Task::deliver()方法中,Swoft会根据当前环境隐式切换投递方式。但该消息队列的实现依赖Semaphore拓展,如果你想使用,需要在编译PHP时加上--enable-sysvmsg参数。

定时任务

除了手动执行的普通任务,Swoft还提供了精度为秒的定时任务功能用来在项目中替代Linux的Crontab功能.

Swoft用两个前置Process---任务计划进程:CronTimerProcess和任务执行进程CronExecProcess
,和两张内存数据表-----RunTimeTable(任务(配置)表)OriginTable((任务)执行表)用于定时任务的管理调度。
两张表的每行记录的结构如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

\\Swoft\Task\Crontab\TableCrontab.php

/**

 * 任务表,记录用户配置的任务信息

 * 表每行记录包含的字段如下,其中`rule`,`taskClass`,`taskMethod`生成key唯一确定一条记录

 * @var array $originStruct

 */

private $originStruct = [

    'rule'       => [\Swoole\Table::TYPE_STRING, 100],//定时任务执行规则,对应@Scheduled注解的cron属性

    'taskClass'  => [\Swoole\Table::TYPE_STRING, 255],//任务名 对应@Task的name属性(默认为类名)

    'taskMethod' => [\Swoole\Table::TYPE_STRING, 255],//Task方法,对应@Scheduled注解所在方法

    'add_time'   => [\Swoole\Table::TYPE_STRING, 11],//初始化该表内容时的10位时间戳

];

/**

 * 执行表,记录短时间内要执行的任务列表及其执行状态

 * 表每行记录包含的字段如下,其中`taskClass`,`taskMethod`,`minute`,`sec`生成key唯一确定一条记录

 * @var array $runTimeStruct

 */

private $runTimeStruct = [

    'taskClass'  => [\Swoole\Table::TYPE_STRING, 255],//同上

    'taskMethod' => [\Swoole\Table::TYPE_STRING, 255],//同上

    'minute'      => [\Swoole\Table::TYPE_STRING, 20],//需要执行任务的时间,精确到分钟 格式date('YmdHi')

    'sec'        => [\Swoole\Table::TYPE_STRING, 20],//需要执行任务的时间,精确到分钟 10位时间戳

    'runStatus'  => [\Swoole\TABLE::TYPE_INT, 4],//任务状态,有 0(未执行)  1(已执行)  2(执行中) 三种。

    //注意:这里的执行是一个容易误解的地方,此处的执行并不是指任务本身的执行,而是值`任务投递`这一操作的执行,从宏观上看换成 _未投递_,_已投递_,_投递中_描述会更准确。

];

此处为何要使用Swoole的内存Table?

Swoft的的定时任务管理是分别由 任务计划进程 和 任务执行进程 进程负责的。两个进程的运行共同管理定时任务,如果使用进程间独立的array()等结构,两个进程必然需要频繁的进程间通信。而使用跨进程的Table(本文的Table,除非特别说明,都指SwooleSwoole\Table结构)直接进行进程间数据共享,不仅性能高,操作简单 还解耦了两个进程。

为了Table能够在两个进程间共同使用,Table必须在Swoole Server启动前创建并分配内存。具体代码在Swoft\Task\Bootstrap\Listeners->onBeforeStart()中,比较简单,有兴趣的可以自行阅读。

背景介绍完了,我们来看看这两个定时任务进程的行为

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

//Swoft\Task\Bootstrap\Process\CronTimerProcess.php

/**

 * Crontab timer process

 *

 * @Process(name="cronTimer", boot=true)

 */

class CronTimerProcess implements ProcessInterface

{

    /**

     * @param \Swoft\Process\Process $process

     */

    public function run(SwoftProcess $process)

    {

        //code....

        /* @var \Swoft\Task\Crontab\Crontab $cron*/

        $cron = App::getBean('crontab');

        // Swoole/HttpServer

        $server = App::$server->getServer();

        $time = (60 - date('s')) * 1000;

        $server->after($time, function () use ($server, $cron) {

            // Every minute check all tasks, and prepare the tasks that next execution point needs

            $cron->checkTask();

            $server->tick(60 * 1000, function () use ($cron) {

                $cron->checkTask();

            });

        });

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

//Swoft\Task\Crontab\Crontab.php

/**

 * 初始化runTimeTable数据

 *

 * @param array $task        任务

 * @param array $parseResult 解析crontab命令规则结果,即Task需要在当前分钟内的哪些秒执行

 * @return bool

 */

private function initRunTimeTableData(array $task, array $parseResult): bool

{

    $runTimeTableTasks = $this->getRunTimeTable()->table;

    $min = date('YmdHi');

    $sec = strtotime(date('Y-m-d H:i'));

    foreach ($parseResult as $time) {

        $this->checkTaskQueue(false);

        $key = $this->getKey($task['rule'], $task['taskClass'], $task['taskMethod'], $min, $time + $sec);

        $runTimeTableTasks->set($key, [

            'taskClass'  => $task['taskClass'],

            'taskMethod' => $task['taskMethod'],

            'minute'     => $min,

            'sec'        => $time + $sec,

            'runStatus'  => self::NORMAL

        ]);

    }

    return true;

}

CronTimerProcessSwoft的定时任务调度进程,其核心方法是Crontab->initRunTimeTableData()
该进程使用了Swoole的定时器功能,通过Swoole\Timer在每分钟首秒时执行的回调,CronTimerProcess每次被唤醒后都会遍历任务表计算出当前这一分钟内的60秒分别需要执行的任务清单,写入执行表并标记为 未执行。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

//Swoft\Task\Bootstrap\Process

/**

 * Crontab process

 *

 * @Process(name="cronExec", boot=true)

 */

class CronExecProcess implements ProcessInterface

{

    /**

     * @param \Swoft\Process\Process $process

     */

    public function run(SwoftProcess $process)

    {

        $pname = App::$server->getPname();

        $process->name(sprintf('%s cronexec process', $pname));

        /** @var \Swoft\Task\Crontab\Crontab $cron */

        $cron = App::getBean('crontab');

        // Swoole/HttpServer

        $server = App::$server->getServer();

        $server->tick(0.5 * 1000, function () use ($cron) {

            $tasks = $cron->getExecTasks();

            if (!empty($tasks)) {

                foreach ($tasks as $task) {

                    // Diliver task

                    Task::deliverByProcess($task['taskClass'], $task['taskMethod']);

                    $cron->finishTask($task['key']);

                }

            }

        });

    }

}

CronExecProcess作为定时任务的执行者,通过Swoole\Timer0.5s唤醒自身一次,然后把 执行表 遍历一次,挑选当下需要执行的任务,通过sendMessage()投递出去并更新该 任务执行表中的状态。
该执行进程只负责任务的投递,任务的实际实际执行仍然在Task进程中由TaskExecutor处理。

定时任务的宏观执行情况如下:

明确的学习思路能更高效的学习

点击加入该群学习 

Swoft源码之Swoole和Swoft的分析的更多相关文章

  1. Swoft 源码剖析 - Swoole和Swoft的那些事 (Http/Rpc服务篇)

    前言 Swoft在PHPer圈中是一个门槛较高的Web框架,不仅仅由于框架本身带来了很多新概念和前沿的设计,还在于Swoft是一个基于Swoole的框架.Swoole在PHPer圈内学习成本最高的工具 ...

  2. swoft| 源码解读系列一: 好难! swoft demo 都跑不起来怎么破? docker 了解一下呗~

    title: swoft| 源码解读系列一: 好难! swoft demo 都跑不起来怎么破? docker 了解一下呗~description: 阅读 sowft 框架源码, swoft 第一步, ...

  3. swoft| 源码解读系列二: 启动阶段, swoft 都干了些啥?

    date: 2018-8-01 14:22:17title: swoft| 源码解读系列二: 启动阶段, swoft 都干了些啥?description: 阅读 sowft 框架源码, 了解 sowf ...

  4. swoft 源码解读【转】

      官网: https://www.swoft.org/ 源码解读: http://naotu.baidu.com/file/814e81c9781b733e04218ac7a0494e2a?toke ...

  5. Java集合源码学习(四)HashMap分析

    ArrayList.LinkedList和HashMap的源码是一起看的,横向对比吧,感觉对这三种数据结构的理解加深了很多. >>数组.链表和哈希表结构 数据结构中有数组和链表来实现对数据 ...

  6. Java集合源码学习(三)LinkedList分析

    前面学习了ArrayList的源码,数组是顺序存储结构,存储区间是连续的,占用内存严重,故空间复杂度很大.但数组的二分查找时间复杂度小,为O(1),数组的特点是寻址容易,插入和删除困难.今天学习另外的 ...

  7. Java集合源码学习(二)ArrayList分析

    >>关于ArrayList ArrayList直接继承AbstractList,实现了List. RandomAccess.Cloneable.Serializable接口,为什么叫&qu ...

  8. Apache Spark源码走读之6 -- 存储子系统分析

    欢迎转载,转载请注明出处,徽沪一郎. 楔子 Spark计算速度远胜于Hadoop的原因之一就在于中间结果是缓存在内存而不是直接写入到disk,本文尝试分析Spark中存储子系统的构成,并以数据写入和数 ...

  9. 【 js 基础 】【 源码学习 】源码设计 (更新了backbone分析)

    学习源码,除了学习对一些方法的更加聪明的代码实现,同时也要学习源码的设计,把握整体的架构.(推荐对源码有一定熟悉了之后,再看这篇文章) 目录结构:第一部分:zepto 设计分析 第二部分:unders ...

随机推荐

  1. 安装配置 Android Studio

    概述 Android Studio 本身应该是开箱即用的,但是由于 dl.google.com 访问太慢,导致了这样那样的问题,因此我们只需要改一下 hosts 就行了 具体步骤 在Ping检测网站查 ...

  2. Leetcode(7)整数反转

    Leetcode(6)Z字形变换 [题目表述]: 给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转. 第一次:转字符串处理 执行用时:40 ms: 内存消耗:11.6MB 效果: ...

  3. 你真的了解java序列化吗

    问:可是我这个实体类,没有实现序列化那个接口,也能存到数据库,这是为什么呢? 想不通!我是用的注解和hibernate框架弄的! 难道说不实现序列化接口也能保存数据?不应该啊. @Entity pub ...

  4. 文件读取(filestream)

    在stream中已经介绍过,文件读取应用filestream,其是以字节为单位读取文件的.在操作中,当应用filestream创建文件流,读取时应先定义一个字节数组,在转化成char类型,最后转化成s ...

  5. QlikSense 2018.2月版起支持主题开发

    自定义主题开发 // 主题是qliksense 2018年2月版提出,4月版正式实施,其实就是去修改sense默认的.json文件和.css文件 { // 定义自定义主题是否从默认主题(Sense C ...

  6. fenby C语言 P9

    逻辑运算 真1 假0 &&与 真&&真为真 真&&假为假 假&&真为假 假&&假为假 ||或 真||真为真 真||假为真 ...

  7. 如何在sprintf函数中输出百分号(%)等特殊符号

    php中的sprinf可以格式化字符串的数据类型.今天遇到了想在其中输出%,可难倒我了. $query =  sprintf("select * from books where %s li ...

  8. VS Code 1.40 发布!可自行搭建 Web 版 VS Code!

    今天(北京时间 2019 年 11 月 8 日),微软发布了 Visual Studio Code 1.40 版本.让我们来看看有哪些主要的更新. 自建 Web 版 VS Code 前不久,微软正式发 ...

  9. PHP输出A到Z及相关

    先看以下一段PHP的代码,想下输出结果是什么. <?php for($i='A'; $i<='Z'; $i++) { echo $i . '<br>'; } ?> 输出的 ...

  10. Redis开发与运维:SDS

    STRING 我们会经常打交道的string类型,在redis中拥有广泛的使用.也是开启redis数据类型的基础. 在我最最开始接触的redis的时候,总是以为字符串类型就是值的类型是字符串. 比如: ...