首发原文链接:Swoole 源码分析之 Coroutine 协程模块

大家好,我是码农先森。

引言

协程又称轻量级线程,但与线程不同的是;协程是用户级线程,不需要操作系统参与。由用户显式控制,可以在需要的时候挂起、或恢复执行。

通过协程程序可以在执行的过程中保存当前的状态,并在恢复后从该状态处继续执行,整体上来说创建、销毁、切换的成本低。

但在 Swoole 中的协程是无法利用多核 CPU 的,如果想利用多核 CPU 则需要依赖 Swoole 的多进程模型。

协程的出现为 Swoole 程序提升并发效率、及系统的处理能力,注入了强劲的动力;可以说是 Swoole 作为高性能通信框架的的核心模块。

源码拆解

这次我们以下面这段代码,来作为本次拆解源码的切入点。

// 协程容器
Swoole\Coroutine\run(function () {
// Socket 协程客户端
$socket = new Swoole\Coroutine\Socket(AF_INET, SOCK_STREAM, 0);
// 建立连接,在建立连接的过程中会发生协程切换
$retval = $socket->connect('127.0.0.1', 9601);
if ($retval) {
// 发送数据,在发送数据的过程中会发生协程切换
$n = $socket->send('hello');
var_dump($n); // 解释数据,在接收数据的过程中会发生协程切换
$data = $socket->recv();
var_dump($data); // 关闭连接
$socket->close();
}
});

这段代码主要是使用 Socket 的协程客户端与本地的 9601 端口建立连接,并且发送、接收数据。在分析源码之前,我对这次的源码做了一个图解梳理,把整个调用链路上的函数串联了起来。我们可以先对整体有个大致的了解,便于后面分析源代码。

Socket 协程客户端

Socket 协程客户端是专门用于 Swoole 在协程环境中使用的,可以实现在 IO 调用时切换协程,让出 CPU 的使用权。例如:在连接建立、发送数据、接收数据 等阶段会进行协程的切换。

这个函数主要是发起 Socket 连接的建立,并且在 wait_event 这个函数内部实现了协程的切换。

// swoole-src/src/coroutine/socket.cc:595
bool Socket::connect(const struct sockaddr *addr, socklen_t addrlen) {
if (sw_unlikely(!is_available(SW_EVENT_RDWR))) {
return false;
}
int retval;
do {
// 发起连接建立
retval = ::connect(sock_fd, addr, addrlen);
} while (retval < 0 && errno == EINTR);
if (retval < 0) {
if (errno != EINPROGRESS) {
set_err(errno);
return false;
} else {
TimerController timer(&write_timer, connect_timeout, this, timer_callback);
// wait_event 这个函数内部实现了协程的切换
if (!timer.start() || !wait_event(SW_EVENT_WRITE)) {
if (is_closed()) {
set_err(ECONNABORTED);
}
return false;
} else {
if (socket->get_option(SOL_SOCKET, SO_ERROR, &errCode) < 0 || errCode != 0) {
set_err(errCode);
return false;
}
}
}
}
connected = true;
set_err(0);
return true;
}

再看看 wait_event 函数的内部实现,先是获取到当前的协程,然后根据事件的类型调用函数 add_event 将事件添加到事件管理的结构体中,最后将当前的协程切换出去,让出其 CPU 的控制权。

// swoole-src/src/coroutine/socket.cc:147
bool Socket::wait_event(const EventType event, const void **__buf, size_t __n) {
EventType added_event = event;
// 获取到当前的协程
Coroutine *co = Coroutine::get_current_safe();
if (!co) {
return false;
}
if (sw_unlikely(socket->close_wait)) {
set_err(SW_ERROR_CO_SOCKET_CLOSE_WAIT);
return false;
} // clear the last errCode
set_err(0);
#ifdef SW_USE_OPENSSL
// 根据事件的类型调用函数 add_event 将事件添加到事件管理的结构体中
if (sw_unlikely(socket->ssl && ((event == SW_EVENT_READ && socket->ssl_want_write) ||
(event == SW_EVENT_WRITE && socket->ssl_want_read)))) {
if (sw_likely(socket->ssl_want_write && add_event(SW_EVENT_WRITE))) {
want_event = SW_EVENT_WRITE;
} else if (socket->ssl_want_read && add_event(SW_EVENT_READ)) {
want_event = SW_EVENT_READ;
} else {
return false;
}
added_event = want_event;
} else
#endif
if (sw_unlikely(!add_event(event))) {
return false;
}
swoole_trace_log(SW_TRACE_SOCKET,
"socket#%d blongs to cid#%ld is waiting for %s event",
sock_fd,
co->get_cid(),
get_wait_event_name(this, event)); Coroutine::CancelFunc cancel_fn = [this, event](Coroutine *co) { return cancel(event); }; // 将当前的协程切换出去,让出其 CPU 的控制权
if (sw_likely(event == SW_EVENT_READ)) {
read_co = co;
read_co->yield(&cancel_fn);
read_co = nullptr;
} else if (event == SW_EVENT_WRITE) {
if (sw_unlikely(!zero_copy && __n > 0 && *__buf != get_write_buffer()->str)) {
write_buffer->clear();
if (write_buffer->append((const char *) *__buf, __n) != SW_OK) {
set_err(ENOMEM);
goto _failed;
}
*__buf = write_buffer->str;
}
write_co = co;
write_co->yield(&cancel_fn);
write_co = nullptr;
} else {
assert(0);
return false;
}
_failed:
#ifdef SW_USE_OPENSSL
// maybe read_co and write_co are all waiting for the same event when we use SSL
if (sw_likely(want_event == SW_EVENT_NULL || !has_bound()))
#endif
{
Reactor *reactor = SwooleTG.reactor;
if (sw_likely(added_event == SW_EVENT_READ)) {
reactor->remove_read_event(socket);
} else {
reactor->remove_write_event(socket);
}
}
#ifdef SW_USE_OPENSSL
want_event = SW_EVENT_NULL;
#endif
swoole_trace_log(SW_TRACE_SOCKET,
"socket#%d blongs to cid#%ld trigger %s event",
sock_fd,
co->get_cid(),
get_trigger_event_name(this, added_event));
return !is_closed() && !errCode;
}

同理 send()recv() 函数,也和 connect() 函数是一样的实现方式。

// swoole-src/src/coroutine/socket.cc:847
ssize_t Socket::send(const void *__buf, size_t __n) {
if (sw_unlikely(!is_available(SW_EVENT_WRITE))) {
return -1;
}
ssize_t retval;
TimerController timer(&write_timer, write_timeout, this, timer_callback);
do {
// 发送数据
retval = socket->send(__buf, __n, 0);
} while (retval < 0 && socket->catch_write_error(errno) == SW_WAIT && timer.start() &&
wait_event(SW_EVENT_WRITE, &__buf, __n));
check_return_value(retval);
return retval;
} // swoole-src/src/coroutine/socket.cc:874
ssize_t Socket::recv(void *__buf, size_t __n) {
if (sw_unlikely(!is_available(SW_EVENT_READ))) {
return -1;
}
ssize_t retval;
TimerController timer(&read_timer, read_timeout, this, timer_callback);
do {
// 接收数据
retval = socket->recv(__buf, __n, 0);
} while (retval < 0 && socket->catch_read_error(errno) == SW_WAIT && timer.start() && wait_event(SW_EVENT_READ));
check_return_value(retval);
return retval;
}

也是调用 wait_event() 函数来实现当前的协程切换,唯一的区别就是事件的类型不同,一个是读事件,一个是写事件。

Run 协程容器

在 Swoole 中要想使用协程,那么必须要在协程的环境中使用协程的客户端,或者支持 Hook 的原生 PHP 函数。才能享受到 Swoole 中协程带来的高性能,不然和普通的 PHP 执行程序没有什么区别,变成了同步阻塞。

在源码中协程容器主要是实现了事件循环的初始化、协程上下文的创建管理、事件循环的 IO 事件监听,接下来我们会主要分析关于事件管理的部分内容。

// swoole-src/src/coroutine/base.cc:210
namespace coroutine {
bool run(const CoroutineFunc &fn, void *arg) {
// 事件循环的初始化
if (swoole_event_init(SW_EVENTLOOP_WAIT_EXIT) < 0) {
return false;
}
// 协程上下文的创建管理
Coroutine::activate();
long cid = Coroutine::create(fn, arg);
// 事件循环的 IO 事件监听
swoole_event_wait();
Coroutine::deactivate();
return cid > 0;
}
}

Event 事件初始化

Event 事件初始化主要是定义一些事件的回调函数,便于在事件被触发时恢复对应的协程进行后续的逻辑处理,例如:读事件回调函数 readable_event_callback、写事件回调函数 writable_event_callback 等。

// swoole-src/src/wrapper/event.cc:37
int swoole_event_init(int flags) {
if (!SwooleG.init) {
std::unique_lock<std::mutex> lock(init_lock);
swoole_init();
} // 创建一个 Reactor 实例对象
Reactor *reactor = new Reactor(SW_REACTOR_MAXEVENTS);
if (!reactor->ready()) {
return SW_ERR;
} if (flags & SW_EVENTLOOP_WAIT_EXIT) {
reactor->wait_exit = 1;
} // Socket 事件初始化
coroutine::Socket::init_reactor(reactor);
coroutine::System::init_reactor(reactor);
network::Client::init_reactor(reactor); SwooleTG.reactor = reactor; return SW_OK;
}
// swoole-src/include/swoole_coroutine_sokcet.h:157
static inline void init_reactor(Reactor *reactor) {
// 定义对应事件的回调函数
reactor->set_handler(SW_FD_CO_SOCKET | SW_EVENT_READ, readable_event_callback);
reactor->set_handler(SW_FD_CO_SOCKET | SW_EVENT_WRITE, writable_event_callback);
reactor->set_handler(SW_FD_CO_SOCKET | SW_EVENT_ERROR, error_event_callback);
}
// swoole-src/src/coroutine/socket.c:48
int Socket::readable_event_callback(Reactor *reactor, Event *event) {
Socket *socket = (Socket *) event->socket->object;
socket->set_err(0);
#ifdef SW_USE_OPENSSL
if (sw_unlikely(socket->want_event != SW_EVENT_NULL)) {
if (socket->want_event == SW_EVENT_READ) {
// 恢复对应的协程
socket->write_co->resume();
}
} else
#endif
{
if (socket->recv_barrier && (*socket->recv_barrier)() && !event->socket->event_hup) {
return SW_OK;
}
// 恢复对应的协程
socket->read_co->resume();
} return SW_OK;
}

Event 事件监听

Event 事件监听主要是针对被加入到事件循环中的 Socket 进行 IO 事件的监听,如果有读或写IO事件的触发,则回调到对应的处理函数上进行执行。

// swoole-src/src/warpper/event.cc:84
int swoole_event_wait() {
Reactor *reactor = SwooleTG.reactor;
int retval = 0;
if (!reactor->wait_exit or !reactor->if_exit()) {
// 事件循环等待调用
retval = reactor->wait(nullptr);
}
swoole_event_free();
return retval;
}
// swoole-src/src/reactor/epoll.cc:153
int ReactorEpoll::wait(struct timeval *timeo) {
Event event;
ReactorHandler handler;
int i, n, ret; int reactor_id = reactor_->id;
int max_event_num = reactor_->max_event_num; if (reactor_->timeout_msec == 0) {
if (timeo == nullptr) {
reactor_->timeout_msec = -1;
} else {
reactor_->timeout_msec = timeo->tv_sec * 1000 + timeo->tv_usec / 1000;
}
} reactor_->before_wait(); while (reactor_->running) {
if (reactor_->onBegin != nullptr) {
reactor_->onBegin(reactor_);
}
// 监听 IO 事件
n = epoll_wait(epfd_, events_, max_event_num, reactor_->get_timeout_msec());
if (n < 0) {
if (!reactor_->catch_error()) {
swoole_sys_warning("[Reactor#%d] epoll_wait failed", reactor_id);
return SW_ERR;
} else {
goto _continue;
}
} else if (n == 0) {
reactor_->execute_end_callbacks(true);
SW_REACTOR_CONTINUE;
}
for (i = 0; i < n; i++) {
event.reactor_id = reactor_id;
event.socket = (Socket *) events_[i].data.ptr;
event.type = event.socket->fd_type;
event.fd = event.socket->fd; if (events_[i].events & (EPOLLRDHUP | EPOLLERR | EPOLLHUP)) {
event.socket->event_hup = 1;
}
// read 读事件,这里的 handler 对应 readable_event_callback
if ((events_[i].events & EPOLLIN) && !event.socket->removed) {
handler = reactor_->get_handler(SW_EVENT_READ, event.type);
ret = handler(reactor_, &event);
if (ret < 0) {
swoole_sys_warning("EPOLLIN handle failed. fd=%d", event.fd);
}
}
// write 写事件,这里的 handler 对应 writable_event_callback
if ((events_[i].events & EPOLLOUT) && !event.socket->removed) {
handler = reactor_->get_handler(SW_EVENT_WRITE, event.type);
ret = handler(reactor_, &event);
if (ret < 0) {
swoole_sys_warning("EPOLLOUT handle failed. fd=%d", event.fd);
}
}
// error 错误处理,这里的 handler 对应 error_event_callback
if ((events_[i].events & (EPOLLRDHUP | EPOLLERR | EPOLLHUP)) && !event.socket->removed) {
// ignore ERR and HUP, because event is already processed at IN and OUT handler.
if ((events_[i].events & EPOLLIN) || (events_[i].events & EPOLLOUT)) {
continue;
}
handler = reactor_->get_error_handler(event.type);
ret = handler(reactor_, &event);
if (ret < 0) {
swoole_sys_warning("EPOLLERR handle failed. fd=%d", event.fd);
}
}
if (!event.socket->removed && (event.socket->events & SW_EVENT_ONCE)) {
reactor_->_del(event.socket);
}
} _continue:
reactor_->execute_end_callbacks(false);
SW_REACTOR_CONTINUE;
}
return 0;
}

总结

  • 协程又称轻量级线程,协程是用户级线程;不需要操作系统参与,创建切换成本低。
  • Swoole 中的协程是无法利用多核 CPU 的,如果想利用多核 CPU 则需要依赖 Swoole 的多进程模型。
  • Swoole 中协程的是利用的 Event 事件循环进行调度的,将遇到 IO 操作的 Socket 统一加入到事件循环中。
  • 本次的源码分析旨在了解整个协程在 Swoole 中的运行逻辑,打开我们的思路,便于我们更好的体会到协程所带来的高性能价值。

Swoole 源码分析之 Coroutine 协程模块的更多相关文章

  1. 一个普通的 Zepto 源码分析(二) - ajax 模块

    一个普通的 Zepto 源码分析(二) - ajax 模块 普通的路人,普通地瞧.分析时使用的是目前最新 1.2.0 版本. Zepto 可以由许多模块组成,默认包含的模块有 zepto 核心模块,以 ...

  2. 一个普通的 Zepto 源码分析(三) - event 模块

    一个普通的 Zepto 源码分析(三) - event 模块 普通的路人,普通地瞧.分析时使用的是目前最新 1.2.0 版本. Zepto 可以由许多模块组成,默认包含的模块有 zepto 核心模块, ...

  3. jQuery 源码分析(十九) DOM遍历模块详解

    jQuery的DOM遍历模块对DOM模型的原生属性parentNode.childNodes.firstChild.lastChild.previousSibling.nextSibling进行了封装 ...

  4. jQuery 源码分析(二十一) DOM操作模块 删除元素 详解

    本节说一下DOM操作模块里的删除元素模块,该模块用于删除DOM里的某个节点,也可以理解为将该节点从DOM树中卸载掉,如果该节点有绑定事件,我们可以选择保留或删除这些事件,删除元素的接口有如下三个: e ...

  5. jQuery 源码分析(二十) DOM操作模块 插入元素 详解

    jQuery的DOM操作模块封装了DOM模型的insertBefore().appendChild().removeChild().cloneNode().replaceChild()等原生方法.分为 ...

  6. jQuery 源码分析(十二) 数据操作模块 html特性 详解

    jQuery的属性操作模块总共有4个部分,本篇说一下第1个部分:HTML特性部分,html特性部分是对原生方法getAttribute()和setAttribute()的封装,用于修改DOM元素的特性 ...

  7. jQuery 源码分析(十五) 数据操作模块 val详解

    jQuery的属性操作模块总共有4个部分,本篇说一下最后一个部分:val值的操作,也是属性操作里最简单的吧,只有一个API,如下: val(vlaue)        ;获取匹配元素集合中第一个元素的 ...

  8. jQuery 源码分析(十四) 数据操作模块 类样式操作 详解

    jQuery的属性操作模块总共有4个部分,本篇说一下第3个部分:类样式操作部分,用于修改DOM元素的class特性的,对于类样式操作来说,jQuery并没有定义静态方法,而只定义了实例方法,如下: a ...

  9. Swoole 源码分析——Server模块之TaskWorker事件循环

    swManager_start 创建进程流程 task_worker 进程的创建可以分为三个步骤:swServer_create_task_worker 申请所需的内存.swTaskWorker_in ...

  10. Swoole 源码分析——基础模块之 Pipe 管道

    前言 管道是进程间通信 IPC 的最基础的方式,管道有两种类型:命名管道和匿名管道,匿名管道专门用于具有血缘关系的进程之间,完成数据传递,命名管道可以用于任何两个进程之间.swoole 中的管道都是匿 ...

随机推荐

  1. Ez_pycode_dis qsnctfwp

    Python字节码基础 下载相关文件并打开,其中为 Python 字节码. 字节码格式为 源码行号 | 指令在函数中的偏移 | 指令符号 | 指令参数 | 实际参数值 根据上述字节码格式以及文件内容开 ...

  2. Launching Teamviewer remotely through SSH

    Launching Teamviewer remotely through SSH When you need to manage your Server remotely, but you can ...

  3. mysql 必知必会整理—视图[十二]

    前言 简单整理一下视图. 正文 视图: 需要MySQL 5 MySQL 5添加了对视图的支持.因此,本章内容适用于MySQL 5及以后的版本. 视图是虚拟的表.与包含数据的表不一样,视图只包含使用时动 ...

  4. c# .net缓存(旧)

    前言 是迁移以前的blog. 关于c# 缓存在web应用中的一个引导,能够建立起一个缓存的基本思路. System.Web.Caching 这个真的是老生常谈了,我们只需要key和iv,然后我们就可以 ...

  5. oracle 数据库连接

    前言 关于oracle 数据库如何连接,我一开始以为和mysql 和 sql server一样,写好连接语句然后调用相应的dll. 知道我遇到了两个错误: 1.64位程序不能去驱动32位客户端 2.O ...

  6. Redis持久化技术浅析

    Redis是一种内存数据库,数据都存储在内存中,因此可以快速地直接基于内存中的数据结构进行高性能的操作,但是所有数据都在内存中,一旦服务器宕机,内存中的数据就会全部丢失,数据将无法恢复,因此Redis ...

  7. 力扣448(java)-找到数组中所有消失的数(简单)

    题目: 给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内.请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果. 示例 ...

  8. 阿里集团业务驱动的升级 —— 聊一聊Dubbo 3.0 的演进思路

    简介: 阿里云在 2020年底提出了"三位一体"理念,目标是希望将"自研技术"."开源项目"."商业产品"形成统一的技术 ...

  9. 技术解析:一文看懂 Anolis OS 国密生态 | 龙蜥专场

    ​ 简介: Anolis OS国密是社区在Anolis OS上做的国密技术解决方案. 编者注:本文系两位演讲者整理,他们在2021年阿里云开发者大会的「开源操作系统社区和生态分论坛」上带了分享,演讲主 ...

  10. 优秀的 Modbus 从站(从机、服务端)仿真器、串口调试工具

    目录 优秀的 Modbus 从站(从机.服务端)仿真器.串口调试工具 主要功能 软件截图 优秀的 Modbus 从站(从机.服务端)仿真器.串口调试工具 官网下载地址:http://www.redis ...