由于Redis的多线程IO代码较多,故分成了三篇博客讲解
IO线程的分配
上面我们已经知道了IO线程的初始化、IO线程的运行函数IOThreadMain主要处理逻辑,以及延迟读写的客户端是何时分别加入到server全局变量的clients_pending_read和clients_pending_write中的,接下来去看下时何时为客户端分配IO线程。
在aeProcessEvents处理事件的函数中,等待事件产生之前,调用了beforeSleep(networking.c)方法,beforeSleep中又调用了handleClientsWithPendingReadsUsingThreads(为延迟读取操作的客户端分配线程)
void beforeSleep(struct aeEventLoop *eventLoop) {
UNUSED(eventLoop);
// 省略...
handleBlockedClientsTimeout();
/* 调用了handleClientsWithPendingReadsUsingThreads为延迟读客户端分配线程 */
handleClientsWithPendingReadsUsingThreads();
// 省略...
/* 调用了handleClientsWithPendingWritesUsingThreads为延迟写客户端分配线程 */
handleClientsWithPendingWritesUsingThreads();
// 省略...
}
延迟读操作的客户端分配线程
handleClientsWithPendingReadsUsingThreads 定义如下
int handleClientsWithPendingReadsUsingThreads(void) {
if (!server.io_threads_active || !server.io_threads_do_reads) return 0;
int processed = listLength(server.clients_pending_read);
if (processed == 0) return 0;
listIter li;
listNode *ln;
// 获取待读取的客户端列表clients_pending_read加入到迭代链表中
listRewind(server.clients_pending_read,&li);
int item_id = 0;
// 遍历待读取的客户端
while((ln = listNext(&li))) {
// 获取客户端
client *c = listNodeValue(ln);
// 根据线程数取模,轮询分配线程
int target_id = item_id % server.io_threads_num;
// 分配线程,加入到线程对应的io_threads_list
listAddNodeTail(io_threads_list[target_id],c);
item_id++;
}
/* 将线程的操作状态置为读操作*/
io_threads_op = IO_THREADS_OP_READ;
// 遍历线程数
for (int j = 1; j < server.io_threads_num; j++) {
// 获取每个线程待处理客户端的个数
int count = listLength(io_threads_list[j]);
// 将待处理客户端的个数设置到线程对应的io_threads_pending[j]中,io_threads_pending数组中记录了每个线程要处理的客户端个数
setIOPendingCount(j, count);
}
/* 获取io_threads_list[0]中待处理的客户端列表,io_threads_list[0]存储的是主线程的数据*/
/* handleClientsWithPendingReadsUsingThreads函数的执行者刚好就是主线程,所以让主线程处理io_threads_list[0]中的数据*/
listRewind(io_threads_list[0],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
// 调用readQueryFromClient
readQueryFromClient(c->conn);
}
listEmpty(io_threads_list[0]);
/* 等待其他线程处理完毕 */
while(1) {
unsigned long pending = 0;
for (int j = 1; j < server.io_threads_num; j++)
// 获取每一个客户端处理的客户端个数
pending += getIOPendingCount(j);
// 如果为0表示所有线程对应的客户端都处理完毕
if (pending == 0) break;
}
/* 再次判断server.clients_pending_read是否有待处理的客户端*/
while(listLength(server.clients_pending_read)) {
// 获取列表第一个元素
ln = listFirst(server.clients_pending_read);
// 获取客户端
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_READ;
// 删除节点
listDelNode(server.clients_pending_read,ln);
serverAssert(!(c->flags & CLIENT_BLOCKED));
// processPendingCommandsAndResetClient函数中会判断客户端标识是否是CLIENT_PENDING_COMMAND状态,如果是调用processCommandAndResetClient函数处理请求命令
if (processPendingCommandsAndResetClient(c) == C_ERR) {
continue;
}
// 由于客户端输入缓冲区可能有其他的命令未读取,这里解析命令并执行
processInputBuffer(c);
if (!(c->flags & CLIENT_PENDING_WRITE) && clientHasPendingReplies(c))
clientInstallWriteHandler(c);
}
/* Update processed count on server */
server.stat_io_reads_processed += processed;
return processed;
}
主要逻辑如下:
- 从server.clients_pending_read获取延迟读取操作的客户端,将其加入到迭代列表
- 遍历延迟读操作的客户端列表,获取每一个待处理的客户端client,item_id表示每个客户端的序号,从0开始,每处理一个客户端就增1,用序号对线程数server.io_threads_num取模,得到一个target_id,客户端会被加入到io_threads_list[target_id]对应的列表中,也就是使用取模的方式轮询为每一个客户端分配对应线程,然后将客户端加入到该线程待处理的客户端列表中,此时客户端已分配到线程,在线程的运行函数IOThreadMain会调用readQueryFromClient处理客户端数据,需要注意多线程只是从客户端数据读取数据解析命令,并不会执行命令,在processInputBuffer中可以看到在IO多线程下只会将flags状态标记为CLIENT_PENDING_COMMAND,不会执行processCommandAndResetClient函数:
void processInputBuffer(client *c) {
while(c->qb_pos < sdslen(c->querybuf)) {
// 省略...
if (c->argc == 0) {
resetClient(c);
} else {
/* 在IO多线程情况下不能在这里执行命令,所以在这里将client标记为CLIENT_PENDING_COMMAND然后返回,等待主线程同步执行命令 */
if (c->flags & CLIENT_PENDING_READ) {
c->flags |= CLIENT_PENDING_COMMAND;
break;
}
/* 准备执行命令 */
if (processCommandAndResetClient(c) == C_ERR) {
return;
}
}
}
// 省略...
}
- 将io_threads_op线程操作状态置为读操作
- 遍历线程数,获取每一个线程要处理的客户端个数,将其设置到线程对应的io_threads_pending[j]中,io_threads_pending数组中记录了每个线程等待处理的客户端个数
- 获取io_threads_list[0]中待处理的客户端列表,io_threads_list[0]存储的是主线程的数据,因为当前执行handleClientsWithPendingReadsUsingThreads函数的线程正是主线程,所以让主线程来处理io_threads_list[0]中存放的待处理客户端
- 主线程遍历io_threads_list[0]中每一个待处理的客户端,调用readQueryFromClient处理,从客户端读取数据
- 主线程开启一个while(1)循环等待其他IO线程处理完毕,结束条件是pending为0,pending记录了所有线程要处理的客户端数量总和,在前面IOThreadMain函数中可以看到线程在处理完毕之后会将对应io_threads_pending数组中记录的个数置为0,当pending为0表示所有的线程都已将各自复制的客户端数据处理完毕
- 主线程开启while循环准备执行客户端命令(注意这里才开始执行命令,多线程只负责解析不负责执行),循环条件是server.clients_pending_read列表的长度不为0,主线程需要保证客户端的请求顺序,所从clients_pending_read列表中的第一个元素开始向后遍历:
(1)调用listNodeValue获取列表中的元素,也就是待处理的客户端client
(2)调用listDelNode将获取到的元素从列表删除,因为在第7步中,主线程已经等待其他所有的线程执行完毕,此时所有的线程已经将各自负责的客户端数据处理完成,所以可以将客户端从server.clients_pending_read中移除
(3)调用processPendingCommandsAndResetClient函数判断客户端标识是否是CLIENT_PENDING_COMMAND状态,CLIENT_PENDING_COMMAND状态表示客户端的请求命令已经被IO线程解析(processInputBuffer方法中可以看到状态被标记为CLIENT_PENDING_COMMAND),可以开始执行命令,接着调用processCommandAndResetClient函数执行客户端发送的请求命令
(4)由于客户端输入缓冲区可能有其他的命令未读取,这里调用processInputBuffer处理输入缓冲区数据继续解析命令并执行
processPendingCommandsAndResetClient
processPendingCommandsAndResetClient函数在networking.c中,它先判断客户端标识是否是CLIENT_PENDING_COMMAND状态,CLIENT_PENDING_COMMAND状态表示客户端的请求命令已经被IO线程解析,可以被执行,所以如果处于CLIENT_PENDING_COMMAND状态,接下来会调用processCommandAndResetClient函数处理客户端命令,具体是调用processCommand函数执行命令的:
/*processPendingCommandsAndResetClient函数(networking.c中) */
int processPendingCommandsAndResetClient(client *c) {
// 判断客户端标识是否是CLIENT_PENDING_COMMAND
if (c->flags & CLIENT_PENDING_COMMAND) {
// 取消CLIENT_PENDING_COMMAND状态
c->flags &= ~CLIENT_PENDING_COMMAND;
// 调用processCommandAndResetClient执行命令
if (processCommandAndResetClient(c) == C_ERR) {
return C_ERR;
}
}
return C_OK;
}
/* processCommandAndResetClient函数(networking.c中) */
int processCommandAndResetClient(client *c) {
int deadclient = 0;
client *old_client = server.current_client;
server.current_client = c;
// 调用processCommand执行命令
if (processCommand(c) == C_OK) {
commandProcessed(c);
}
if (server.current_client == NULL) deadclient = 1;
server.current_client = old_client;
return deadclient ? C_ERR : C_OK;
}
processCommand
processCommand函数在server.c文件中,它调用了addReply函数将需要返回给客户端的数据先写入缓冲区:
int processCommand(client *c) {
// 省略...
if (!strcasecmp(c->argv[0]->ptr,"quit")) {
// 调用addReply函数将需要返回给客户端的数据先写入缓冲区
addReply(c,shared.ok);
c->flags |= CLIENT_CLOSE_AFTER_REPLY;
return C_ERR;
}
// 省略...
}
数据读取的整体过程如下,IO多线程只是负责从客户端读取数据解析命令,执行命令的过程仍然是单线程的:
延迟写操作的客户端分配线程
handleClientsWithPendingWritesUsingThreads 定义如下
int handleClientsWithPendingWritesUsingThreads(void) {
int processed = listLength(server.clients_pending_write);
if (processed == 0) return 0;
if (server.io_threads_num == 1 || stopThreadedIOIfNeeded()) {
return handleClientsWithPendingWrites();
}
if (!server.io_threads_active) startThreadedIO();
listIter li;
listNode *ln;
// 获取待写回客户端列表clients_pending_write加入到迭代链表中
listRewind(server.clients_pending_write,&li);
int item_id = 0;
// 遍历待写的客户端
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_WRITE;
if (c->flags & CLIENT_CLOSE_ASAP) {
listDelNode(server.clients_pending_write, ln);
continue;
}
// 根据线程数取模,轮询分配线程
int target_id = item_id % server.io_threads_num;
// 分配线程,加入到对应线程的io_threads_list
listAddNodeTail(io_threads_list[target_id],c);
item_id++;
}
/* 将io_threads_op线程操作状态置为写操作 */
io_threads_op = IO_THREADS_OP_WRITE;
for (int j = 1; j < server.io_threads_num; j++) {
int count = listLength(io_threads_list[j]);
// 设置每个线程需要处理的客户端个数
setIOPendingCount(j, count);
}
/* 获取io_threads_list[0]中待处理的客户端列表,io_threads_list[0]存储的是主线程的数据*/
/* handleClientsWithPendingWritesUsingThreads函数的执行者刚好就是主线程,所以让主线程处理io_threads_list[0]中的数据*/
listRewind(io_threads_list[0],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
// 调用writeToClient往客户写数据
writeToClient(c,0);
}
listEmpty(io_threads_list[0]);
/* 等待其他线程处理完毕 */
while(1) {
unsigned long pending = 0;
for (int j = 1; j < server.io_threads_num; j++)
pending += getIOPendingCount(j);
if (pending == 0) break;
}
/* 再次获取server.clients_pending_read所有待写的客户端*/
listRewind(server.clients_pending_write,&li);
// 遍历
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
/* 如果缓冲区数据未全部写回调用connSetWriteHandler注册可写事件,回调函数为sendReplyToClient*/
if (clientHasPendingReplies(c) &&
connSetWriteHandler(c->conn, sendReplyToClient) == AE_ERR)
{
freeClientAsync(c);
}
}
// 清空clients_pending_write
listEmpty(server.clients_pending_write);
server.stat_io_writes_processed += processed;
return processed;
}
延迟写操作的客户端分配线程在handleClientsWithPendingWritesUsingThreads中实现(networking.c),处理逻辑与handleClientsWithPendingReadsUsingThreads类似:
- 从server.clients_pending_write获取延迟写操作的客户端,将其加入到迭代列表
- 遍历延迟写操作的客户端列表,获取每一个待处理的客户端client,使用取模的方式轮询为每一个客户端分配线程,然后将客户端加入到该线程待处理的客户端列表中,此时客户端已分配到线程,在线程的运行函数IOThreadMain会处待写回数据的客户端
- 将io_threads_op线程操作状态置为写操作
- 遍历线程数,获取每一个线程要处理的客户端个数,将其设置到线程对应的io_threads_pending[j]中,io_threads_pending数组中记录了每个线程等待处理的客户端个数
- 获取io_threads_list[0]中待处理的客户端列表,io_threads_list[0]存储的是主线程的数据,因为当前执行handleClientsWithPendingWritesUsingThreads函数的线程正是主线程,所以让主线程来处理io_threads_list[0]中存放的待处理客户端
- 主线程遍历io_threads_list[0]中每一个待处理的客户端,调用writeToClient往客户端写数据
- 主线程开启一个while(1)循环等待其他IO线程处理完毕
- 主线程开启while循环,循环条件是server.clients_pending_write列表的长度不为0,遍历clients_pending_write中待处理的写客户端:
(1)调用listNodeValue获取待处理的客户端client
(2)判断缓冲区数据是否全部写回到客户端,如果未全部写回调用connSetWriteHandler向内核注册写事件监听,回调函数为sendReplyToClient,待事件循环流程再次执行时,注册的可写事件会通过回调函数sendReplyToClient 处理,把缓冲区中的数据写回客户端。 - 调用listEmpty函数清空server.clients_pending_write列表
connSetWriteHandler
connSetWriteHandler函数在connection.c文件中,它通过set_write_handler注册了写handler,set_write_handler对应的是connSocketSetWriteHandler函数,所以connSetWriteHandler会被映射为connSocketSetWriteHandler,connSocketSetWriteHandler函数调用了aeCreateFileEvent向内核中注册可写事件监听,上面可知回调函数为sendReplyToClient ,等事件循环流程再次执行时,handleClientsWithPendingWritesUsingThreads 函数注册的可写事件会通过回调函数sendReplyToClient 处理,把缓冲区中的数据写回客户端。
ConnectionType CT_Socket = {
.ae_handler = connSocketEventHandler,
.close = connSocketClose,
.write = connSocketWrite,
.read = connSocketRead,
.accept = connSocketAccept,
.connect = connSocketConnect,
.set_write_handler = connSocketSetWriteHandler, // set_write_handler对应connSocketSetWriteHandler函数
.set_read_handler = connSocketSetReadHandler,
.get_last_error = connSocketGetLastError,
.blocking_connect = connSocketBlockingConnect,
.sync_write = connSocketSyncWrite,
.sync_read = connSocketSyncRead,
.sync_readline = connSocketSyncReadLine,
.get_type = connSocketGetType
};
/*
* connSetWriteHandler
*/
static inline int connSetWriteHandler(connection *conn, ConnectionCallbackFunc func) {
// 注册写handler, set_write_handler对应的是connSocketSetWriteHandler函数
return conn->type->set_write_handler(conn, func, 0);
}
/*
* connSocketSetWriteHandler注册写事件
*/
static int connSocketSetWriteHandler(connection *conn, ConnectionCallbackFunc func, int barrier) {
if (func == conn->write_handler) return C_OK;
conn->write_handler = func;
if (barrier)
conn->flags |= CONN_FLAG_WRITE_BARRIER;
else
conn->flags &= ~CONN_FLAG_WRITE_BARRIER;
if (!conn->write_handler)
aeDeleteFileEvent(server.el,conn->fd,AE_WRITABLE);
else
if (aeCreateFileEvent(server.el,conn->fd,AE_WRITABLE, // 向内核注册写事件
conn->type->ae_handler,conn) == AE_ERR) return C_ERR;
return C_OK;
}
总结