推荐大家可以先去看Redis 源码分析 I/O 模型详解,下面有些图我是复制这里面的,自己再画有点重构造轮子
1、首先说说IO多路复用在整个请求链路中所在的位置
用必应搜到的图,
redis的多路复用相当于这张图中的selector(多路复用器),可以有效利用空闲线程,处理读写请求。
是在服务端部署。
2、IO多路复用选择哪个实现的的源码(在ae.c中)
/* Include the best multiplexing layer supported by this system.
* The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
优先级evport->epoll->kqueue->select。其执行的效率也是从高到低 效率也是从高到低,而到底如何选不是人工指定,而是由服务器所部署在哪种服务器上决定,下面列出
- Solaries 10 中的 evport
- Linux 中的 epoll
- macOS/FreeBSD 中的 kqueue,
- select 为最后的默认选项,但是它也是性能最差的
select比其他差的原因是
- 前三个首先都是O(1)的复杂度,最后一个为O(n),
- 前三个的实现函数(第三部分有描述)都使用了特定内核内部的结构,并且能够服务几十万的文件描述符,而select在使用时会扫描全部监听的描述符,并且只能同时服务 1024 个文件描述符
3、模块方法含义解释
之后比较一下各个模块的方法
四个模块都含有的方法是下面这些,相当于模板设计模式,根据部署环境选择不同的实现。
static int aeApiCreate(aeEventLoop *eventLoop)
static int aeApiResize(aeEventLoop *eventLoop, int setsize)
static void aeApiFree(aeEventLoop *eventLoop)
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask)
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
static char *aeApiName(void)
虽然都有这些方法,但是内部细节会有很多不同,详细的介绍看下面的链接
redis中io复用之evport
redis中io复用之epoll
redis中io复用之kqueue
redis中io复用之select
4、6.0版本以后增加了多线程
如果你看过初始化服务器的源代码的话,在main方法的最后有一个
InitServerLast
方法,
/* Some steps in server initialization need to be done last (after modules
* are loaded).服务器初始化中的一些步骤需要最后完成(在加载模块 之后)。
* Specifically, creation of threads due to a race bug in ld.so, in which
* Thread Local Storage initialization collides with dlopen call.
* see: https://sourceware.org/bugzilla/show_bug.cgi?id=19329
具体来说,由于 ld.so 中的竞争错误而创建线程,其中Thread Local Storage 初始化与 dlopen 调用发生冲突。
见:https://sourceware.org/bugzilla/show_bug.cgi?id=19329
*/
void InitServerLast() {
bioInit();
initThreadedIO();
set_jemalloc_bg_thread(server.jemalloc_bg_thread);
server.initial_memory_usage = zmalloc_used_memory();
}
/* Initialize the data structures needed for threaded I/O.
初始化线程 I/O 所需的数据结构*/
void initThreadedIO(void) {
server.io_threads_active = 0; /* We start with threads not active.我们从不活跃的线程开始 */
/* Don't spawn any thread if the user selected a single thread:
* we'll handle I/O directly from the main thread.
如果用户选择了单个线程,则不要生成任何线程:我们将直接从主线程处理 I/O*/
if (server.io_threads_num == 1) return;
if (server.io_threads_num > IO_THREADS_MAX_NUM) {
serverLog(LL_WARNING,"Fatal: too many I/O threads configured. "
"The maximum number is %d.", IO_THREADS_MAX_NUM);
exit(1);
}
/* Spawn and initialize the I/O threads.产生并初始化 I/O 线程 */
for (int i = 0; i < server.io_threads_num; i++) {
/* Things we do for all the threads including the main thread.
我们为包括主线程在内的所有线程做的事情*/
io_threads_list[i] = listCreate();
if (i == 0) continue; /* Thread 0 is the main thread. 线程 0 是主线程*/
/* Things we do only for the additional threads.我们只为附加线程做的事情 */
pthread_t tid;
pthread_mutex_init(&io_threads_mutex[i],NULL);
setIOPendingCount(i, 0);//设置 IO 挂起计数
初始化后将线程暂时锁住
pthread_mutex_lock(&io_threads_mutex[i]); /* Thread will be stopped. 线程将被停止。*/
if (pthread_create(&tid,NULL,IOThreadMain,(void*)(long)i) != 0) {
serverLog(LL_WARNING,"Fatal: Can't initialize IO thread.");
exit(1);
}
io_threads[i] = tid;
}
}
这里就不很详细的深入了,想了解这方面的可以看一下Redis6.0多线程
但是需要注意的是:虽然是加了多线程,但是实际命令的执行还是单线程(队列) 的形式(每个线程执行readQueryFromClient
,将对应的请求放入一个队列中,单线程执行),主要是把client到输入缓冲区和结果到输出缓冲区这两段用的是多线程,原因有两方面考虑:
- 单线程实际执行可以避免处理锁和竞争的问题
- 并且读写缓冲区的时间IO远远大于命令实际执行的时间