记一个多线程使用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 ...
随机推荐
- Codeforces 618D Hamiltonian Spanning Tree(树的最小路径覆盖)
题意:给出一张完全图,所有的边的边权都是 y,现在给出图的一个生成树,将生成树上的边的边权改为 x,求一条距离最短的哈密顿路径. 先考虑x>=y的情况,那么应该尽量不走生成树上的边,如果生成树上 ...
- 【Revit API】Revit读取当前rvt的所有视图与其名称
1)读取所有视图: public static ViewSet GetAllViews(Document doc) { ViewSet views = new ViewSet(); FilteredE ...
- 【BZOJ1176】Mokia(CDQ分治)
[BZOJ1176]Mokia(CDQ分治) 题面 BZOJ权限题啊,,,, dbzoj真好 Description 维护一个W*W的矩阵,初始值均为S.每次操作可以增加某格子的权值,或询问某子矩阵的 ...
- 使用SetupDI* API列举系统中的设备
原文链接地址:https://blog.csdn.net/clteng/article/details/801012?utm_source=blogxgwz8 在Windows系统中提供一组有用的函数 ...
- ms17-010漏洞扫描工具
说明: 1.先利用masscan进行445端口探测 2.利用巡风的脚本对开放445端口的IP进行ms17-010漏洞扫描. 3.使用方法:Python2运行后,按提示输入单个IP或者IP网段. # c ...
- [pool www] user has not been defined
[02-Dec-2014 00:28:58] ALERT: [pool www] user has not been defined [02-Dec-2014 00:28:58] ERROR: fai ...
- Stanford机器学习---第十四讲.机器学习应用举例之Photo OCR
http://blog.csdn.net/l281865263/article/details/50278745 本栏目(Machine learning)包括单参数的线性回归.多参数的线性回归.Oc ...
- Qt ------ stylesheet 样式
1.所有的窗口组件都可以用 setStyleSheet() 设置样式 2.使用样式,显示效果可以不受平台影响,比如保证window 7 和 linux 显示效果是一样的 QVariant 如果 sty ...
- 基于packstack的openstack单节点安装
一.安装源处理 1.更新base源为网易的源 cd /etc/yum.repos.d/ wget http://mirrors.163.com/.help/CentOS6-Base-163.repo ...
- python---twisted的使用,使用其模拟Scrapy
twisted的网络使用 twisted的异步使用 一:简单使用 from twisted.internet import defer from twisted.web.client import g ...