序列文章:

Task 的实现在 Celery 中你会发现有两处,一处位于 celery/app/task.py,这是第一个;第二个位于 celery/task/base.py 中,这是第二个。他们之间是有关系的,你可以认为第一个是对外暴露的接口,而第二个是具体的实现!所以,我们由简入繁,先来看看对外的接口:

其实这就是个我们声明 Task 的对象,例如我们使用这么一段代码:

我们可以看看 add 对象是啥:

In [1]: add
Out[1]: <@task: worker.add of tasks:0x10c9b06d0>

你会发现其实他就是我们的一个 Task 对象,所以你就可以观察一下我们平时使用这个 add 的形式在里面是如何实现的了,例如我们最常使用的可能就两种方式了,分别是:

In [2]: add.delay()
In [3]: add.apply_async()

其他你看一下源码就会发现他们的实现是一样的,就像这样:

我们现在很清楚,调用 apply_async 是将我们的 Task 提交到 MQ 中,然后获得一个 celery.result.AsyncResult 对象,那么具体都做了哪些工作,还是需要进一步查看的。apply_async 的参数有很多,所以我们需要先给他归个类,这样就好看多了,概括着看,可以分为这么几类:

  • AMQP 类:connection、queue、exchange、routing_key、producer、publisher、headers
  • MQ 策略类:countdown、eta、expires、retry、retry_policy、priority、
  • 管理类:shadow、serializer、compression、add_to_parent
  • 其他:args、kwargs、link、link_error、

这样一看就感觉一目了然多了,AMQP 类的我们就不关注了,毕竟都看了这么多了,应该大家都熟悉了。这里的主要关注点还是在 MQ 策略类和管理类上,着重在 MQ 策略类上,因为管理类的功能稍微比较简单一些。

async 发送消息

apply_async 中,我们可以看到有两处执行逻辑,第一处是:

这里是直接调用 apply,然后这里的条件 task_always_eager 是什么意思我们还没见过,可以看一下文档:

ok,了解,其实就是说这是个同步的接口,那么我们就可以对应着看到下面这处应该是异步的实现咯:

既然如此,我们一个个得来看。

同步发送消息实现

同步执行消息的一层函数比较简单,只是简单的构建了一个 tracer,然后就从 tracer 调用中拿到调用结果,我们看上去会比较舒服:

但是,这个 tracer 的内容就复杂啦,但是这个 build_tracer 的构建函数不需要太过关注,所以我们需要关注的是 build_tracer 返回的这个 tracer 函数,但是这个函数的内容很多,为了简约一下,所以给大家抽象了一番。同步调用过程中,可以分为几部分功能,分别是:

  • 信号处理:执行前/后/成功这几个时刻需要释放一些信号给感兴趣的成员
  • 失败处理:对于没有执行的情况需要进行细分处理,例如:reject/ignore/retry/exception 等
  • 依赖处理:因为 Celery 支持一些简单的依赖,所以执行完成之后需要执行被依赖的 tasks
  • 执行逻辑:这个就是正常的函数调用咯
  • 其他:例如统计执行时间,出入栈之类的

我们就看下任务的执行逻辑是怎么样的,在代码里面是很简单的一个函数调用,其实就是看 Task 对象有没有实现 __call__ 方法,如果没有就使用 run 方法:

那 task 的 __call__ 实现也不是太复杂,其实最后调用的也是 run 方法,所以到最后都还是 run 方法的责任啦,但是,这里的基类是不实现 run 方法的,所以这个实现就落实到具体的实现类中了,那么你以为 run 方法会在 celery/app/base.py 中实现?我之前也是这么想的,但是,后来我发现不是的,这个实现其实就是我们在代码里面使用 @app.task 装饰的函数,其实就是讲我们自定义的函数封装成 run,这样调用 run 不就执行的我们的函数了吗?有意思吧,这个封装的方式我们后面再说,也就是说同步的方式我们就到此吧,也差不多了。

异步发送消息实现

看完同步的我们再来看看异步的,在说异步的之前,我们先思考一下,异步的应该是怎样?之前看的时候我猜想异步不就是把 Task 对象塞进 MQ 中么,就应该是这么简单,但是,看完之后发现还是 too young 了,因为从同步中我们就可以看出,还是有很多功课要完成的,不管怎样,一起来再看一遍。

从前边我们说有同步和异步两种形式那里我们可以发现同步和异步的除了功能不一样之外,还有调用的对象也不一样,同步的是调用 Task 自己的方法,也就是说消息被 Task 自己消化了;而异步的确实使用的 Celery 对象的方法,也就是说还得依赖于 Celery 这个 Boss 来实现。这是为啥呢?很明显嘛,Task 自身没有关于 MQ 的任何消息,而只有一个绑定的 Celery 对象,所以从抽象层面就只能交给 Celery 了,而 Celery 却包含了所有你需要的信息,是可以完成这个任务的。

所以,异步的消息到了 Celery 是这么被发出去的:

这里出现了一个我们还没怎么接触过的 amqp,但是没关系,随着等下的了解,我们会认识到它的,这里的几个关键步骤都是通过 amqp 来完成的,所以我们应该着重看看他们

异步消息体的创建

在 Celery 中,异步消息体是通过 create_task_message 来创建的,我们可以发现,这里是传了一大堆参数进去,但是,无妨,对于这些参数,我们大部分都在前面见过了,不怵,主要还是需要关注一下内部都为消息体做了什么工作:

这里可以发现两件事情

  1. 消息体的预处理都是在这里完成的,例如检验和转换参数格式
  2. 构建消息就用了四个属性:headerspropertiesbodysent_event

这里其实就是所有的构建消息体的代码了,为什么呢,因为 task_message 是一个 nametuple:

异步消息的发送

异步消息的发送这里不是直接就调用的一个函数,而是动态得创建了一个 sender ,然后才调用这个 sender 发送的(没搞懂为啥,为了扩展?)。而创建 sender 的逻辑倒是比较简单,所以忽略了,直接来看真正的 send 操作是如何完成的,其实之前提过了,这里真正的 send 操作就像之前我们看同步的执行逻辑一样尿性,又臭又长,真的,又臭又长,而且作者自己都加注释承认了,他的理由是为了性能!

同样得,为了方便我们的理解,我还是采用抽丝剥茧的方式来给大家介绍一下,首先,我习惯性得分个类:

  • MQ 的各项功能:routing_key/exchange/delivery_mode/retry
  • 任务执行的前后处理:发送前/发送后
  • 真正的发送逻辑
  • 其他

其实重头戏应该在 MQ 的参数确定上,因为只要这些参数都确定了,消息的发送只是一个 producer.publish 就解决的事情,所以我们花些精力来看看 MQ 的参数都是怎么决定出来的:

  • queue_name
    1. 调用 task.delay 的时候传的,没传并且也没传 exchange 那就是 default
    2. 不会出现传了 exchange 但是不传 queue
  • routing_key
    1. 调用 task.delay 的时候传的,没传就看 exchange 有没有,没有就是 queue 的值了
    2. 如果参数传了 exchange,那么就是配置中的默认 routing_key
  • exchange:
    1. 调用 task.delay 的时候传的,没传但是 exchange_type 类型是 direct,那么就是 ""
    2. 如果类型不是 direct,那么 queue 有 exchange 就用,没有就使用默认的
  • delivery_mode
    1. 调用 task.delay 的时候传的,没传就看 queue 里面有没有,有就用
    2. 没有就使用默认的
  • retry:
    1. 调用 task.delay 的时候传了就用,没传就用默认的

等这些参数确认完之后,就使用这些参数发送了!

然后这样子就将消息发出去了,等待 Worker 的接收,而 worker 的接受逻辑我们之前已经看到了,其实还是注册的 Consumer 的 on_message

附加

在前面我们说如何构建异步消息体的时候,对于消息体只是简单的用几个 ... 忽略过,但是,对于整体理解来说,我们不应该忽略他们的实质内容,所以在最后我把他们都罗列出来,前后的会用到的。而且你会发现有点意思的是,对于我们的一个异步调用,task 名和 id 都是放在 headers 里头的,而参数什么的却是放在 body 里面,在我自己实现的异步 MQ 里面,这些都是放在 body 里面的,这点我倒是不太欣赏 Celery 的。

headers

properties

body

send_event

Celery 源码解析三: Task 对象的实现的更多相关文章

  1. Celery 源码解析五: 远程控制管理

    今天要聊的话题可能被大家关注得不过,但是对于 Celery 来说确实很有用的功能,曾经我在工作中遇到这类情况,就是我们将所有的任务都放在同一个队列里面,然后有一天突然某个同学的代码写得不对,导致大量的 ...

  2. Celery 源码解析六:Events 的实现

    在 Celery 中,除了远程控制之外,还有一个元素可以让我们对分布式中的任务的状态有所掌控,而且从实际意义上来说,这个元素对 Celery 更为重要,这就是在本文中将要说到的 Event. 在 Ce ...

  3. Mybatis源码解析(三) —— Mapper代理类的生成

    Mybatis源码解析(三) -- Mapper代理类的生成   在本系列第一篇文章已经讲述过在Mybatis-Spring项目中,是通过 MapperFactoryBean 的 getObject( ...

  4. AspNetCore3.1_Secutiry源码解析_2_Authentication_核心对象

    系列文章目录 AspNetCore3.1_Secutiry源码解析_1_目录 AspNetCore3.1_Secutiry源码解析_2_Authentication_核心项目 AspNetCore3. ...

  5. ReactiveCocoa源码解析(三) Signal代码的基本实现

    上篇博客我们详细的聊了ReactiveSwift源码中的Bag容器,详情请参见<ReactiveSwift源码解析之Bag容器>.本篇博客我们就来聊一下信号量,也就是Signal的的几种状 ...

  6. ReactiveSwift源码解析(三) Signal代码的基本实现

    上篇博客我们详细的聊了ReactiveSwift源码中的Bag容器,详情请参见<ReactiveSwift源码解析之Bag容器>.本篇博客我们就来聊一下信号量,也就是Signal的的几种状 ...

  7. React的React.createRef()/forwardRef()源码解析(三)

    1.refs三种使用用法 1.字符串 1.1 dom节点上使用 获取真实的dom节点 //使用步骤: 1. <input ref="stringRef" /> 2. t ...

  8. Celery 源码解析四: 定时任务的实现

    在系列中的第二篇我们已经看过了 Celery 中的执行引擎是如何执行任务的,并且在第三篇中也介绍了任务的对象,但是,目前我们看到的都是被动的任务执行,也就是说目前执行的任务都是第三方调用发送过来的.可 ...

  9. Spring源码解析三:IOC容器的依赖注入

    一般情况下,依赖注入的过程是发生在用户第一次向容器索要Bean是触发的,而触发依赖注入的地方就是BeanFactory的getBean方法. 这里以DefaultListableBeanFactory ...

随机推荐

  1. Reliability diagrams

    Reliability diagrams (Hartmann et al. 2002) are simply graphs of the Observed frequency of an event ...

  2. vscode 调试.net core 2.0 输出乱码解决方法

    之前在vscode上调试.net core 2.0项目时输出窗口一直是乱码,查了很多资料无法解决 最终在github找到了解决办法 ->   https://github.com/OmniSha ...

  3. HelloWorld改编,仿bilibili手机端(一)——侧滑菜单界面布局

    讲解目录: 1.要实现的效果图展示及详细分析HelloWorld项目的xml布局文件(基于navigation drawer activity)        2.简单修改menu及menu相关详解 ...

  4. 入门VMware Workstation下的Debian学习之Vim简单使用(三)

    什么是Vim? Vim具有程序编辑的能力,可以主动的以字体颜色辨别语法的正确性,方便程序设计. Vim是从vi发展出来的一个文本编辑器.代码补完.编译及错误跳转等方便编程的功能特别丰富,在程序员中被广 ...

  5. js-异步机制与同步机制

    Javascript的优势之一是其如何处理异步代码.异步代码会被放入一个事件队列,等到所有其他代码执行后才进行,而不会阻塞线程 1 理解异步代码: 1.1 JavaScript最基础的异步函数是set ...

  6. Hadoop(四)HDFS集群详解

    前言 前面几篇简单介绍了什么是大数据和Hadoop,也说了怎么搭建最简单的伪分布式和全分布式的hadoop集群.接下来这篇我详细的分享一下HDFS. HDFS前言: 设计思想:(分而治之)将大文件.大 ...

  7. XML编程

     XML编程 XML及其语法 XML约束之DTD XML编程(CRUD---Create Read Update Delete) XML约束之Schema XML语法: XML文档的组成: 一个X ...

  8. Linq常见操作示例

    static void DeferredQuery() { var names = new List<string> { "Nino", "Alberto&q ...

  9. commons-pool与commons-pool2连接池

    commons-pool和commons-pool2是用来建立对象池的框架,提供了一些将对象池化必须要实现的接口和一些默认动作.对象池化之后可以通过pool的概念去管理其生命周期,例如对象的创建,使用 ...

  10. SAP问题【转载】

    1.A:在公司代码分配折旧表时报错? 在公司代码分配折旧表时报错,提示是"3000 的公司代码分录不完全-参见长文本" 希望各位大侠帮我看看. 3000 的公司代码分录不完全-参见 ...