记一个多线程使用libevent的问题
前段时间使用libevent网络库实现了一个游戏服务器引擎,在此记录下其中遇到的一个问题。
我在设计服务器上选择把逻辑和网络分线程,线程之间通信使用队列。但是这样做会有个问题:
当逻辑线程想要主动的发一个数据包的时候,网络线程此时可能还阻塞在等待网络IO的系统调用上(比如说epoll)。如果不做特殊处理的话,此时消息包就会一直积压在缓冲区中,直到下一次网络线程从挂起的系统调用返回(比如来了一个消息包)。因此,当逻辑线程发送消息包的时候(bufferevent_write)需要一种唤醒机制,让网络线程从epoll等系统调用中返回并处理发送消息包逻辑。
由于对libevent的api不熟悉,起初我是自己实现这个功能的。实现确实是不复杂,但是缺违背了我的初心:只写简单必要的代码,保证尽可能少的bug。直到后来和同事探讨(争论:P)了一番,才发现原来libevent是有对此做支持的,但是具体怎么做,文档里面没有详细的说,因此同事也说不出个所以然。鉴于此情况,我决定,把libevent中与此相关的源码粗略的过一遍,以求能弄明白以下两件事:
(1)与跨线程唤醒事件等待相关的api有哪些,以及如何使用?
(2)这些api背后到底做了哪些工作?
相关API,及用法
和我起初想得不一样,libevent相关的api很简单并且只有一个:
/*call event_use_pthreads() if use posix threads.*/
evthread_use_windows_threads();
struct event_base* ev_base = event_base_new();
需要注意的是函数evthread_use_windows_threads的调用必须在初始化event_base之前。在此之后,无需再做别的事情,逻辑线程在执行bufferevent_write的时候,libevent就会自动唤醒网络线程的事件循环,并执行发送数据。
隐藏在API背后的逻辑
先看看evthread_use_windows_threads函数做了什么?
int evthread_use_windows_threads(void) {
...
evthread_set_lock_callbacks(&cbs);
evthread_set_id_callback(evthread_win32_get_id);
evthread_set_condition_callbacks(&cond_cbs);
return 0;
}
通过调用evthread_use_windows_threads,我们设置了一些回调函数,包括设置了libevent获取线程id的回调函数evthread_id_fn_。
看看初始化事件循环的函数event_base_new做了什么:
// event.c
struct event_base *
event_base_new(void) {
...
base = event_base_new_with_config(cfg);
} struct event_base *
event_base_new_with_config(const struct event_config *cfg) {
...
#ifndef EVENT__DISABLE_THREAD_SUPPORT
if (EVTHREAD_LOCKING_ENABLED() &&
(!cfg || !(cfg->flags & EVENT_BASE_FLAG_NOLOCK))) {
int r;
EVTHREAD_ALLOC_LOCK(base->th_base_lock, 0);
EVTHREAD_ALLOC_COND(base->current_event_cond);
r = evthread_make_base_notifiable(base);
if (r<0) {
event_warnx("%s: Unable to make base notifiable.", __func__);
event_base_free(base);
return NULL;
}
}
#endif
...
} int
evthread_make_base_notifiable(struct event_base *base) {
int r;
if (!base)
return -1; EVBASE_ACQUIRE_LOCK(base, th_base_lock);
r = evthread_make_base_notifiable_nolock_(base);
EVBASE_RELEASE_LOCK(base, th_base_lock);
return r;
} static int
evthread_make_base_notifiable_nolock_(struct event_base *base) {
...
if (evutil_make_internal_pipe_(base->th_notify_fd) == 0) {
notify = evthread_notify_base_default;
cb = evthread_notify_drain_default;
} else {
return -1;
} base->th_notify_fn = notify;
}
它通过如下调用:
event_base_new-->event_base_new_with_config-->evthread_make_base_notifiable-->evthread_make_base_notifiable_nolock_
最后通过evutil_make_internal_pipe_函数创建了两个互相连接的socket(windows环境下,用此来模拟pipe):
/* Internal function: Set fd[0] and fd[1] to a pair of fds such that writes on
* fd[1] get read from fd[0]. Make both fds nonblocking and close-on-exec.
* Return 0 on success, -1 on failure.
*/
int
evutil_make_internal_pipe_(evutil_socket_t fd[2])
{
/*
Making the second socket nonblocking is a bit subtle, given that we
ignore any EAGAIN returns when writing to it, and you don't usally
do that for a nonblocking socket. But if the kernel gives us EAGAIN,
then there's no need to add any more data to the buffer, since
the main thread is already either about to wake up and drain it,
or woken up and in the process of draining it.
*/ #if defined(EVENT__HAVE_PIPE2)
if (pipe2(fd, O_NONBLOCK|O_CLOEXEC) == 0)
return 0;
#endif
#if defined(EVENT__HAVE_PIPE)
if (pipe(fd) == 0) {
if (evutil_fast_socket_nonblocking(fd[0]) < 0 ||
evutil_fast_socket_nonblocking(fd[1]) < 0 ||
evutil_fast_socket_closeonexec(fd[0]) < 0 ||
evutil_fast_socket_closeonexec(fd[1]) < 0) {
close(fd[0]);
close(fd[1]);
fd[0] = fd[1] = -1;
return -1;
}
return 0;
} else {
event_warn("%s: pipe", __func__);
}
#endif #ifdef _WIN32
#define LOCAL_SOCKETPAIR_AF AF_INET
#else
#define LOCAL_SOCKETPAIR_AF AF_UNIX
#endif
if (evutil_socketpair(LOCAL_SOCKETPAIR_AF, SOCK_STREAM, 0, fd) == 0) {
if (evutil_fast_socket_nonblocking(fd[0]) < 0 ||
evutil_fast_socket_nonblocking(fd[1]) < 0 ||
evutil_fast_socket_closeonexec(fd[0]) < 0 ||
evutil_fast_socket_closeonexec(fd[1]) < 0) {
evutil_closesocket(fd[0]);
evutil_closesocket(fd[1]);
fd[0] = fd[1] = -1;
return -1;
}
return 0;
}
fd[0] = fd[1] = -1;
return -1;
}
之后,再设置用于唤醒操作的notify函数(evthread_notify_base_default):
static int
evthread_notify_base_default(struct event_base *base) {
char buf[1];
int r;
buf[0] = (char) 0;
#ifdef _WIN32
r = send(base->th_notify_fd[1], buf, 1, 0);
#else
r = write(base->th_notify_fd[1], buf, 1);
#endif
return (r < 0 && ! EVUTIL_ERR_IS_EAGAIN(errno)) ? -1 : 0;
}
可以看出,在windows下libevent的唤醒机制实际也是self pipe trick,只不过它通过构造一对socket来模拟pipe,当需要唤醒的时候,它就往其中一个socket写入1个字节。
再去看看bufferevent_write:
// bufferevent.c
int
bufferevent_write(struct bufferevent *bufev, const void *data, size_t size) {
if (evbuffer_add(bufev->output, data, size) == -1)
return (-1); return 0;
} // buffer.c
int
evbuffer_add(struct evbuffer *buf, const void *data_in, size_t datlen) {
...
evbuffer_invoke_callbacks_(buf);
}
它会触发一系列列回调函数,而这些回调函数在创建bufferevent的时候被指定:
//bufferevent_sock.c
struct bufferevent *
bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, int options) {
...
evbuffer_add_cb(bufev->output, bufferevent_socket_outbuf_cb, bufev);
} static void
bufferevent_socket_outbuf_cb(struct evbuffer *buf, const struct evbuffer_cb_info *cbinfo, void *arg) {
...
if (cbinfo->n_added &&
(bufev->enabled & EV_WRITE) &&
!event_pending(&bufev->ev_write, EV_WRITE, NULL) &&
!bufev_p->write_suspended) {
/* Somebody added data to the buffer, and we would like to
* write, and we were not writing. So, start writing. */
if (bufferevent_add_event_(&bufev->ev_write, &bufev->timeout_write) == -1) {
/* Should we log this? */
}
}
} //bufferevent.c
int
bufferevent_add_event_(struct event *ev, const struct timeval *tv) {
if (!evutil_timerisset(tv))
return event_add(ev, NULL);
else
return event_add(ev, tv);
} //event.c
int
event_add(struct event *ev, const struct timeval *tv) {
EVBASE_ACQUIRE_LOCK(ev->ev_base, th_base_lock);
res = event_add_nolock_(ev, tv, 0);
EVBASE_RELEASE_LOCK(ev->ev_base, th_base_lock);
} int
event_add_nolock_(struct event *ev, const struct timeval *tv, int tv_is_absolute) {
...
/* if we are not in the right thread, we need to wake up the loop */
//如果在构造event_base之前调用了evthread_use_windows_threads,EVBASE_NEED_NOTIFY此时会返回true,否则为false。
if (res != -1 && notify && EVBASE_NEED_NOTIFY(base))
evthread_notify_base(base);
}
由代码可知,在往bufferevent写数据后执行的回调函数中,就有唤醒网络线程逻辑(evthread_notify_base)。那为什么还需要手动调用evthread_use_windows_threads函数呢?
这里再说一下:
#define EVBASE_NEED_NOTIFY(base) \
((base)->running_loop && \
((base)->th_owner_id != evthreadimpl_get_id_())) unsigned long
evthreadimpl_get_id_() {
return evthread_id_fn_ ? evthread_id_fn_() : 1;
}
之前说过,当调用evthread_use_windows_threads,设置了libevent获取线程id的回调函数evthread_id_fn_。也正因为此,才会去跑下去执行evthread_notify_base函数:
static int
evthread_notify_base(struct event_base *base) {
EVENT_BASE_ASSERT_LOCKED(base);
if (!base->th_notify_fn)
return -1;
if (base->is_notify_pending)
return 0;
base->is_notify_pending = 1;
return base->th_notify_fn(base);
}
所以,当我们在逻辑线程调用bufferevent_write尝试发送一段数据的时候,它会依据以下的调用,通知网络线程:
bufferevent_write-->evbuffer_add-->evbuffer_invoke_callbacks_-->bufferevent_socket_evtbuf_cb_-->bufferevent_add_event_-->event_add-->event_add_nolock_-->evthread_notify_base
以上便是libevent跨线程唤醒的逻辑。
记一个多线程使用libevent的问题的更多相关文章
- 由一个多线程共享Integer类变量问题引起的。。。
最近看到一个多线程面试题,有三个线程分别打印A.B.C,请用多线程编程实现,在屏幕上循环打印10次ABCABC- 看到这个题目,首先想到的是解决方法是定义一个Integer类对象,初始化为0,由3个线 ...
- 用 python 实现一个多线程网页下载器
今天上来分享一下昨天实现的一个多线程网页下载器. 这是一个有着真实需求的实现,我的用途是拿它来通过 HTTP 方式向服务器提交游戏数据.把它放上来也是想大家帮忙挑刺,找找 bug,让它工作得更好. k ...
- python_way ,day11 线程,怎么写一个多线程?,队列,生产者消费者模型,线程锁,缓存(memcache,redis)
python11 1.多线程原理 2.怎么写一个多线程? 3.队列 4.生产者消费者模型 5.线程锁 6.缓存 memcache redis 多线程原理 def f1(arg) print(arg) ...
- 记一个社交APP的开发过程——基础架构选型(转自一位大哥)
记一个社交APP的开发过程——基础架构选型 目录[-] 基本产品形态 技术选型 最近两周在忙于开发一个社交App,因为之前做过一点儿社交方面的东西,就被拉去做API后端了,一个人头一次完整的去搭这么一 ...
- C++实现一个多线程同步方式的协同工作程序示例
多线程并发程序与协同程序其实是不同的概念.多线程并发是多个执行序同时运行,而协同程序是多个执行序列相互协作,同一时刻只有一个执行序列.今天想到的是将两者结合起来,拿现实生活中的例子来说,假设一个班级有 ...
- ExecutorService 建立一个多线程的线程池的步骤
ExecutorService 建立一个多线程的线程池的步骤: 线程池的作用: 线程池功能是限制在系统中运行的线程数. 依据系统的环境情况,能够自己主动或手动设置线程数量.达到执行的最佳效果:少了浪费 ...
- 多线程——继承Thread类实现一个多线程
继承Thread类实现一个多线程 Thread类部分源码: package java.lang; //该类实现了Runnable接口 public class Thread implements Ru ...
- 多线程——实现Runnable接口实现一个多线程
实现Runnable接口实现一个多线程 Runnable接口源码: package java.lang; //Runnable接口源码只有一个run方法 public interface Runnab ...
- 用JAVA写一个多线程程序,写四个线程,其中二个对一个变量加1,另外二个对一个变量减1
package com.ljn.base; /** * @author lijinnan * @date:2013-9-12 上午9:55:32 */ public class IncDecThrea ...
随机推荐
- HDU2486_A simple stone game
这个题目是这样的,一堆石子有n个,首先第一个人开始可以去1-(n-1)个,接下来两人轮流取石子,每个人可取的石子数必须是一个不超过上一次被取的石子的K倍的整数. 现在求对于一堆数量为n的石子是否为必胜 ...
- dom变成jquery对象 先获取dom对象 然后通过$()转换成jquery对象
dom变成jquery对象 先获取dom对象 然后通过$()转换成jquery对象
- 部分NodeJs
一.cnmp的操作: 1.cnmp info jquery查询jquery的版本: 2.cnmp install jquery@1.11.1:安装: 3.cnmp list查询所有下载的内容: 4.c ...
- 3.5 面向连接的运输:TCP
3.5 面向连接的运输:TCP 3.5.1 TCP连接 TCP进行传输之间要进行三次握手建立连接,这个连接不是物理意义上的有一根电线连接,而是应用端两个应用,在逻辑上是已经建立连接了. TCP 不需 ...
- 【BZOJ3162】独钓寒江雪(树哈希,动态规划)
[BZOJ3162]独钓寒江雪(树哈希,动态规划) 题面 BZOJ 题解 忽然翻到这道题目,突然发现就是前几天一道考试题目... 题解: 树哈希,既然只考虑这一棵树,那么,如果两个点为根是同构的, 他 ...
- 【NOIP2017】宝藏(状态压缩,动态规划)
[NOIP2017]宝藏(状态压缩,动态规划) 题面 洛谷 题目描述 参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋, 也给出了这 n 个宝藏屋之间可供开发的 m 条道路 ...
- [ZJOI2015]幻想乡战略游戏——动态点分治
[ZJOI2015]幻想乡战略游戏 带修改下,边点都带权的重心 随着变动的过程中,一些子树内的点经过会经过一些公共边.考虑能不能对这样的子树一起统计. 把树上贡献分块. 考虑点分治算法 不妨先把题目简 ...
- ORACLE获取某个时间段之间的月份列表
返回1-31,或者1-12,或者某个 select rownum from dual connect by rownum<31 就是connect by http://marcospri ...
- ps裁剪一寸照片及换背景色
1.打开ps,把图片拖进去编辑 2.选魔棒工具,在途中选中背景色,鼠标右键,选选择反向 效果如下: 3.按 快捷键 ctrl + j 可以在右侧看到新建出了一个图层 4.选文件,新建,填写长宽,如果 ...
- 很好的c++和Python混合编程文章
c++中嵌入python入门1 本人是用vc2003+python2.5学习的,其它的也应该差不了多少 0. 坏境设置把Python的include/libs目录分别加到vc的include/lib ...