原文首发链接:Swoole 源码分析之 Channel 通道模块

大家好,我是码农先森。

引言

通道,用于协程间通讯,支持多生产者协程和多消费者协程。底层自动实现了协程的切换和调度。

通道与 PHP 的 Array 类似,仅占用内存,没有其他额外的资源申请,所有操作均为内存操作,无 IO 消耗。

底层使用 PHP 引用计数实现,无内存拷贝。即使是传递巨大字符串或数组也不会产生额外性能消耗 channel 基于引用计数实现,是零拷贝的。

源码拆解

Channel 通道需要在协程环境中使用,我们先看下面这段代码,使用 new Channel(1) 创建一个 channel 对象,然后在第一个协程中向通道中推送数据,在第二个协程获取到通道内的数据进行消费。

use Swoole\Coroutine;
use Swoole\Coroutine\Channel;
use function Swoole\Coroutine\run; run(function(){
// 创建 channel 通道对象
$channel = new Channel(1);
Coroutine::create(function () use ($channel) {
for($i = 0; $i < 10; $i++) {
Coroutine::sleep(1.0);
// 向通道内推送数据
$channel->push(['rand' => rand(1000, 9999), 'index' => $i]);
echo "{$i}\n";
}
});
Coroutine::create(function () use ($channel) {
while(1) {
// 从通道中获取数据
$data = $channel->pop(2.0);
if ($data) {
var_dump($data);
} else {
assert($channel->errCode === SWOOLE_CHANNEL_TIMEOUT);
break;
}
}
});
});

在分析源代码之前,我们可以提前看一下源码整体的调用逻辑图,以便我们有个大致的印象。

这段代码主要是在 Swoole 的协程环境中创建 Channel 对象并初始化其容量的逻辑。

// swoole-src/ext-src/swoole-channel.cc:132
static PHP_METHOD(swoole_channel_coro, __construct) {
zend_long capacity = 1; // 解析传入的参数
ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 1)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(capacity)
ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE); if (capacity <= 0) {
capacity = 1;
} // 当前对象对应的 ChannelObject 结构体指针
ChannelObject *chan_t = php_swoole_channel_coro_fetch_object(Z_OBJ_P(ZEND_THIS));
// 为该通道对象分配新的 Channel 实例,并设置其容量为传入的值。
chan_t->chan = new Channel(capacity);
zend_update_property_long(swoole_channel_coro_ce, SW_Z8_OBJ_P(ZEND_THIS), ZEND_STRL("capacity"), capacity);
}

这段代码主要是在 Swoole 的协程环境中向通道中推送数据并对返回结果进行处理的逻辑。

// swoole-src/ext-src/swoole-channel.cc:149
static PHP_METHOD(swoole_channel_coro, push) {
// 获取当前对象的 Channel 实例
Channel *chan = php_swoole_get_channel(ZEND_THIS);
zval *zdata;
double timeout = -1; // 解析传入的参数
ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 2)
Z_PARAM_ZVAL(zdata)
Z_PARAM_OPTIONAL
Z_PARAM_DOUBLE(timeout)
ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE); Z_TRY_ADDREF_P(zdata);
zdata = sw_zval_dup(zdata);
// 向通道中推入数据
if (chan->push(zdata, timeout)) {
zend_update_property_long(
swoole_channel_coro_ce, SW_Z8_OBJ_P(ZEND_THIS), ZEND_STRL("errCode"), Channel::ERROR_OK);
RETURN_TRUE;
} else {
zend_update_property_long(
swoole_channel_coro_ce, SW_Z8_OBJ_P(ZEND_THIS), ZEND_STRL("errCode"), chan->get_error());
Z_TRY_DELREF_P(zdata);
efree(zdata);
RETURN_FALSE;
}
} // swoole-src/coroutine/channel.cc:105
bool Channel::push(void *data, double timeout) {
// 获取当前协程对象 current_co
Coroutine *current_co = Coroutine::get_current_safe();
// 如果通道已关闭
if (closed) {
// 设置错误并返回空指针
error_ = ERROR_CLOSED;
return false;
}
// 如果通道已满或生产者队列不为空,则设置超时消息,并根据传入的超时值添加定时器,等待生产者。
if (is_full() || !producer_queue.empty()) {
TimeoutMessage msg;
msg.error = false;
msg.timer = nullptr;
if (timeout > 0) {
msg.chan = this;
msg.type = PRODUCER;
msg.co = current_co;
// 根据传入的超时值添加定时器
msg.timer = swoole_timer_add(timeout, false, timer_callback, &msg);
} // 挂起生产者协程
yield(PRODUCER); // 如果设置了定时器,则在超时消息中删除定时器
if (msg.timer) {
swoole_timer_del(msg.timer);
} // 如果当前协程被取消
if (current_co->is_canceled()) {
// 设置错误并返回空指针
error_ = ERROR_CANCELED;
return nullptr;
} // 如果发生超时
if (msg.error) {
// 设置错误并返回空指针
error_ = ERROR_TIMEOUT;
return nullptr;
} // 如果通道关闭且为空的情况
if (closed && is_empty()) {
// 设置相应的错误并返回空指针。
error_ = ERROR_CLOSED;
return nullptr;
}
} // 将数据压入数据队列。
data_queue.push(data);
swoole_trace_log(SW_TRACE_CHANNEL, "push data to channel, count=%ld", length()); // 如果消费者队列不为空,则唤醒消费者协程。
if (!consumer_queue.empty()) {
Coroutine *co = pop_coroutine(CONSUMER);
// 恢复消费者协程
co->resume();
}
return true;
}

这段代码主要是在 Swoole 的协程环境中从通道中取出数据并对返回结果进行处理的逻辑。

// swoole-src/ext-src/swoole-channel.cc:175
static PHP_METHOD(swoole_channel_coro, pop) {
// 获取当前对象的 Channel 实例
Channel *chan = php_swoole_get_channel(ZEND_THIS);
// 设置超时变量为-1
double timeout = -1; // 解析一个超时参数
ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 1)
Z_PARAM_OPTIONAL
Z_PARAM_DOUBLE(timeout)
ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE); // 从通道中取出数据,并返回一个 zval 指针
zval *zdata = (zval *) chan->pop(timeout);
// 如果返回的 zval 指针不为空
if (zdata) {
// 将其返回给 PHP 脚本,并释放内存
RETVAL_ZVAL(zdata, 0, 0);
efree(zdata);
zend_update_property_long(
swoole_channel_coro_ce, SW_Z8_OBJ_P(ZEND_THIS), ZEND_STRL("errCode"), Channel::ERROR_OK);
} else {
zend_update_property_long(
swoole_channel_coro_ce, SW_Z8_OBJ_P(ZEND_THIS), ZEND_STRL("errCode"), chan->get_error());
RETURN_FALSE;
}
} // swoole-src/coroutine/channel.cc:55
void *Channel::pop(double timeout) {
// 获取当前协程对象 current_co
Coroutine *current_co = Coroutine::get_current_safe();
// 如果通道已关闭且为空
if (closed && is_empty()) {
// 设置错误并返回空指针
error_ = ERROR_CLOSED;
return nullptr;
}
// 如果通道为空或者消费者队列不为空
if (is_empty() || !consumer_queue.empty()) {
TimeoutMessage msg;
msg.error = false;
msg.timer = nullptr;
if (timeout > 0) {
msg.chan = this;
msg.type = CONSUMER;
msg.co = current_co;
// 根据传入的超时值添加定时器
msg.timer = swoole_timer_add(timeout, false, timer_callback, &msg);
} // 挂起消费者协程
yield(CONSUMER); // 如果设置了定时器,则在超时消息中删除定时器
if (msg.timer) {
swoole_timer_del(msg.timer);
} // 如果当前协程被取消
if (current_co->is_canceled()) {
// 设置错误并返回空指针
error_ = ERROR_CANCELED;
return nullptr;
} // 如果发生超时
if (msg.error) {
// 设置错误并返回空指针
error_ = ERROR_TIMEOUT;
return nullptr;
} // 如果通道关闭且为空的情况
if (closed && is_empty()) {
// 设置相应的错误并返回空指针。
error_ = ERROR_CLOSED;
return nullptr;
}
} // 从数据队列中弹出数据,并返回该数据。
void *data = data_queue.front();
data_queue.pop(); // 如果生产者队列不为空,则唤醒生产者协程
if (!producer_queue.empty()) {
Coroutine *co = pop_coroutine(PRODUCER);
// 恢复到生产者协程
co->resume();
}
return data;
}

这段代码一是针对超时回调处理的处理逻辑,并恢复相关的协程操作。二是实现了协程的挂起操作,并根据不同的类型将当前协程放入不同的队列中,以便后续根据需要恢复执行。

// swoole-src/coroutine/channel.cc:22
void Channel::timer_callback(Timer *timer, TimerNode *tnode) {
TimeoutMessage *msg = (TimeoutMessage *) tnode->data;
msg->error = true;
msg->timer = nullptr;
if (msg->type == CONSUMER) {
// 从消费者队列中移除该协程
msg->chan->consumer_remove(msg->co);
} else {
// 从生产者队列中移除该协程
msg->chan->producer_remove(msg->co);
}
// 恢复协程
msg->co->resume();
} // swoole-src/coroutine/channel.cc:34
void Channel::yield(enum Opcode type) {
// 获取当前协程
Coroutine *co = Coroutine::get_current_safe();
if (type == PRODUCER) {
// 将当前协程放入到生产者队列
producer_queue.push_back(co);
swoole_trace_log(SW_TRACE_CHANNEL, "producer cid=%ld", co->get_cid());
} else {
// 将当前协程放入到消费者队列
consumer_queue.push_back(co);
swoole_trace_log(SW_TRACE_CHANNEL, "consumer cid=%ld", co->get_cid());
} // 挂起被取消,则调用该函数
Coroutine::CancelFunc cancel_fn = [this, type](Coroutine *co) {
if (type == CONSUMER) {
consumer_remove(co);
} else {
producer_remove(co);
}
co->resume();
return true;
}; // 挂起当前协程
co->yield(&cancel_fn);
}

总结

  1. Channel 通道需要在协程的环境中进行使用,通道是纯内存操作,没有 IO 消耗,非常高效。
  2. 底层使用 Channel::yield 函数实现了协程的自动切换和调度,如果通道处理超时则会自动调用 Channel::timer_callback 函数。
  3. Channel 通道是跨协程直接通信的一大利器,在实际的场景中使用起来十分的便利、高效。

Swoole 源码分析之 Channel 通道模块的更多相关文章

  1. NIO 源码分析(05) Channel 源码分析

    目录 一.Channel 类图 二.begin 和 close 是什么 2.1 AbstractInterruptibleChannel 中的 begin 和 close 2.2 Selector 中 ...

  2. jQuery1.9.1源码分析--数据缓存Data模块

    jQuery1.9.1源码分析--数据缓存Data模块 阅读目录 jQuery API中Data的基本使用方法介绍 jQuery.acceptData(elem)源码分析 jQuery.data(el ...

  3. jQuery 源码分析(十) 数据缓存模块 data详解

    jQuery的数据缓存模块以一种安全的方式为DOM元素附加任意类型的数据,避免了在JavaScript对象和DOM元素之间出现循环引用,以及由此而导致的内存泄漏. 数据缓存模块为DOM元素和JavaS ...

  4. Hadoop2源码分析-HDFS核心模块分析

    1.概述 这篇博客接着<Hadoop2源码分析-RPC机制初识>来讲述,前面我们对MapReduce.序列化.RPC进行了分析和探索,对Hadoop V2的这些模块都有了大致的了解,通过对 ...

  5. Tornado源码分析 --- 静态文件处理模块

    每个web框架都会有对静态文件的处理支持,下面对于Tornado的静态文件的处理模块的源码进行分析,以加强自己对静态文件处理的理解. 先从Tornado的主要模块 web.py 入手,可以看到在App ...

  6. Python 源码分析:queue 队列模块

    起步 queue 模块提供适用于多线程编程的先进先出(FIFO)数据结构.因为它是线程安全的,所以多个线程很轻松地使用同一个实例. 源码分析 先从初始化的函数来看: 从这初始化函数能得到哪些信息呢?首 ...

  7. jQuery 源码分析(十六) 事件系统模块 底层方法 详解

    jQuery事件系统并没有将事件监听函数直接绑定到DOM元素上,而是基于数据缓存模块来管理监听函数的,事件模块代码有点多,我把它分为了三个部分:分底层方法.实例方法和便捷方法.ready事件来讲,好理 ...

  8. jQuery 源码分析(十三) 数据操作模块 DOM属性 详解

    jQuery的属性操作模块总共有4个部分,本篇说一下第2个部分:DOM属性部分,用于修改DOM元素的属性的(属性和特性是不一样的,一般将property翻译为属性,attribute翻译为特性) DO ...

  9. jQuery源码分析(九) 异步队列模块 Deferred 详解

    deferred对象就是jQuery的回调函数解决方案,它解决了如何处理耗时操作的问题,比如一些Ajax操作,动画操作等.(P.s:紧跟上一节:https://www.cnblogs.com/grea ...

  10. WebRTC源码分析四:视频模块结构

    转自:http://blog.csdn.net/neustar1/article/details/19492113 本文在上篇的基础上介绍WebRTC视频部分的模块结构,以进一步了解其实现框架,只有了 ...

随机推荐

  1. MySQL集群入门(PXC)

    目标: 1.掌握PXC集群MySQL方案的原理: 2.掌握PXC集群的强一致性: 3.掌握PXC集群的高可用方案:硬件要求: 1.Win10x64企业版/linux/MacOS: 2.Docker虚拟 ...

  2. c# MVC BundleConfig详解

    前言 因为有很多库在.net core还没有实现迁移,所以呢,我们有时候还是需要的. 这些事什么意思呢? 举一个例子: bundles.Add(new StyleBundle("~/Cont ...

  3. tracer 原理

    前言 准备整理网络章节,先整理概念. tracer 是一个可以获取我们的主机到访问端中间经过了哪些路由,这个对于我们非常重要,看下原理吧. 正文 tracer 在unix中是tracerRout. 它 ...

  4. leetcode:1381. 设计一个支持增量操作的栈

    1381. 设计一个支持增量操作的栈 请你设计一个支持下述操作的栈. 实现自定义栈类 CustomStack : CustomStack(int maxSize):用 maxSize 初始化对象,ma ...

  5. CentOS7下安装Elasticsearch-7.3.2和Elasticsearch-head

    下载Elasticsearch-7.3.2-linux-x86_64.tar.gzElasticsearch下载地址:https://www.elastic.co/cn/downloads/elast ...

  6. 说说你对Node.js 的理解?优缺点?应用场景?

    一.是什么 Node.js 是一个开源与跨平台的 JavaScript 运行时环境 在浏览器外运行 V8 JavaScript 引擎(Google Chrome 的内核),利用事件驱动.非阻塞和异步输 ...

  7. 力扣378(java&python)-有序矩阵中第 K 小的元素(中等)

    题目: 给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素.请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素. 你必须找到一个 ...

  8. Understand Abstraction and Interface

    Foreword 抽象和接口是Java中的两个关键字,也是两种最基本的优化软件项目手段.为什么说它们是一种优化项目的手段? 人分三六九等,不同等级的人,所接触的事和处理的事是不一样的.同理,项目也分大 ...

  9. 进一步释放技术红利,阿里云推出全新内存增强型实例re6,性能提升30%

    5月7日,国内最大云计算厂商阿里云宣布推出全新一代内存增强型实例,提供1:14.8超大内存比内存容量,满足内存型数据库如SAP HANA.Redis等应用,充分释放技术红利,帮助线下企业快速上云,完成 ...

  10. 深度解析|基于 eBPF 的 Kubernetes 一站式可观测性系统

    ​简介:阿里云 Kubernetes 可观测性是一套针对 Kubernetes 集群开发的一站式可观测性产品.基于 Kubernetes 集群下的指标.应用链路.日志和事件,阿里云 Kubernete ...