当前位置: 首页 > news >正文

Redis 6.5 服务端开启多线程源码

redis支持开启多线程,只有从socket到读取缓冲区和从输出缓冲区到socket这两段过程是多线程,而命令的执行还是单线程,并且是由主线程执行
借鉴:【Redis】事件驱动框架源码分析(多线程)

  • 一、main启动时初始化多线程
  • 二、多线程(非主线程)执行函数IOThreadMain
  • 三、主线程会把客户端放入到两个等待客户端列表
    • 1、开启多线程后主线程执行读取事件,如果当前多线程状态是空闲,会把客户端放入等待写入客户端列表并返回
    • 2、开启多线程后,主线程执行完命令把数据写回客户端输出缓冲区并且把此客户端写入等待输出客户端列表
  • 四、给两个等待客户端端列表分配多线程执行
    • 1、给等待写入客户端列表分配线程后,多线程只执行到把数据从socket写入到客户端输出缓冲区,不会执行客户端输入缓冲区命令,当多线程都执行完毕后,由主线程执行各个客户端输入缓冲区的命令
      • (1)多线程只执行到从socket到客户端的输入缓冲区
      • (2) 多线程之后完毕后,主线程执行客户端的缓冲区的命令
    • 2、给等待输出客户端列表分配线程后,多线程(包含主线程)开始执行把从客户端输出缓冲区写入到socket,如果没有写完,还会注册文件事件,让aeMain执行,

在这里插入图片描述

一、main启动时初始化多线程

int main(int argc, char **argv) {//.....删除干扰代码//里面有初始化IO多线程的方法InitServerLast();//.....删除干扰代码
}    
void InitServerLast(void) {//.....删除干扰代码//初始化IO多线程initThreadedIO();//.....删除干扰代码
}

下面initThreadedIO函数中通过pthread_create创建线程,其中IOThreadMain是线程的执行函数

typedef struct __attribute__((aligned(CACHE_LINE_SIZE))) threads_pending {redisAtomic unsigned long value;
} threads_pending;
//存储创建的线程,和io_threads_list数组的下标对应
pthread_t io_threads[IO_THREADS_MAX_NUM];
//存储每个线程要等待处理的客户端个数,和io_threads下标对应,每一个元素中有原子类型value,代表此线程有多少等待处理的客户端
threads_pending io_threads_pending[IO_THREADS_MAX_NUM];
//多线程的状态,比如如果是read,所有的多线程都是read,如果是write,则所有的多线程都是write
int io_threads_op;      /* IO_THREADS_OP_IDLE, IO_THREADS_OP_READ or IO_THREADS_OP_WRITE. */ // TODO: should access to this be atomic??!/* This is the list of clients each thread will serve when threaded I/O is* used. We spawn io_threads_num-1 threads, since one is the main thread* itself. */
//存储每一个线程要处理的客户端,io_threads_list是数组,通过下标标志不同的线程,0是主线程,数组每一个元素是某一个线程可以处理的客户端集合,
list *io_threads_list[IO_THREADS_MAX_NUM];
void initThreadedIO(void) {server.io_threads_active = 0; /* We start with threads not active. *//* Indicate that io-threads are currently idle *///设置IO线程当前状态为空闲io_threads_op = IO_THREADS_OP_IDLE;/* Don't spawn any thread if the user selected a single thread:* we'll handle I/O directly from the main thread. *///如果IO线程为1,即只需要主线程,直接返回即可if (server.io_threads_num == 1) return;//如果IO线程超过最大线程,即128,则报错退出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. *///根据设置的线程数初始化线程数组io_threads_list,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();//如果i=0代表当前创建的线程是主线程,可以跳过if (i == 0) continue; /* Thread 0 is the main thread. *//* Things we do only for the additional threads. */pthread_t tid;pthread_mutex_init(&io_threads_mutex[i],NULL);//设置线程的待处理客户端为0,原因是刚启动,没有一个客户端连接setIOPendingCount(i, 0);pthread_mutex_lock(&io_threads_mutex[i]); /* Thread will be stopped. *///创建线程,线程的运行函数是IOThreadMain,pthread_create是创建线程的函数,并且在创建线程时就执行IOThreadMain方法,不需要再单独启动if (pthread_create(&tid,NULL,IOThreadMain,(void*)(long)i) != 0) {serverLog(LL_WARNING,"Fatal: Can't initialize IO thread.");exit(1);}//把创建的线程标识加入io_threads,tid这个指针变量可以指向创建的线程io_threads[i] = tid;}
}

二、多线程(非主线程)执行函数IOThreadMain

void *IOThreadMain(void *myid) {/* The ID is the thread number (from 0 to server.io_threads_num-1), and is* used by the thread to just manipulate a single sub-array of clients. */long id = (unsigned long)myid;char thdname[16];snprintf(thdname, sizeof(thdname), "io_thd_%ld", id);redis_set_thread_title(thdname);redisSetCpuAffinity(server.server_cpulist);makeThreadKillable();//下面是死循环while(1) {/* Wait for start */for (int j = 0; j < 1000000; j++) {//如果当前线程有需要读取或者输出的客户端,则跳出这个for循环,执行下面的操作if (getIOPendingCount(id) != 0) break;}/* Give the main thread a chance to stop this thread. *///如果上面的for循环是正常执行结束,不是break的,不能后续的操作,通过continue重新回到上面for循环继续等待if (getIOPendingCount(id) == 0) {pthread_mutex_lock(&io_threads_mutex[id]);pthread_mutex_unlock(&io_threads_mutex[id]);continue;}serverAssert(getIOPendingCount(id) != 0);/* Process: note that the main thread will never touch our list* before we drop the pending count to 0. */listIter li;listNode *ln;//把当前线程可以处理的客户端放入到迭代列表中listRewind(io_threads_list[id],&li);while((ln = listNext(&li))) {client *c = listNodeValue(ln);//多线程的状态是输出,从输出缓冲区输出到客户端的socketif (io_threads_op == IO_THREADS_OP_WRITE) {writeToClient(c,0);} else if (io_threads_op == IO_THREADS_OP_READ) {//多线程的状态是读取,从客户端的socket中读取数据到输入缓冲区readQueryFromClient(c->conn);} else {serverPanic("io_threads_op value is unknown");}}listEmpty(io_threads_list[id]);setIOPendingCount(id, 0);}
}

三、主线程会把客户端放入到两个等待客户端列表

通过下面的代码,知道等待写入客户端列表等待输出客户端列表全局变量

struct redisServer {// 省略...//list类型,记录待处理输出的客户端集合list *clients_pending_write; /* There is to write or install handler. *///list类型,记录待处理读取的客户端集合list *clients_pending_read;  /* Client has pending read socket buffers. */// 省略...
}

1、开启多线程后主线程执行读取事件,如果当前多线程状态是空闲,会把客户端放入等待写入客户端列表并返回

在前面的文章中,主线程通过readQueryFromClient函数来执行从客户端socket中得到数据写入输入缓冲区

void readQueryFromClient(connection *conn) {//省略。。。。/* Check if we want to read from the client later when exiting from* the event loop. This is the case if threaded I/O is enabled. */if (postponeClientRead(c)) return;//省略。。。。
}    
/* Return 1 if we want to handle the client read later using threaded I/O.* This is called by the readable handler of the event loop.* As a side effect of calling this function the client is put in the* pending read clients and flagged as such. */
int postponeClientRead(client *c) {if (server.io_threads_active &&// I/O 线程是否活跃,活跃表示激活了IO多线程server.io_threads_do_reads &&//I/O 线程是否执行读操作,表示IO多线程可以针对待读取客户端列表的执行读取操作//不在阻塞期间处理事件,为了避免读取RDB或AOF文件时阻塞无法及时处理请求,processEventsWhileBlokced函数在执行时,待读取客户端列表不能读取。!ProcessingEventsWhileBlocked &&// 客户端标志位不包含 MASTER、SLAVE 或 BLOCKED,解释:CLIENT_MASTER、CLIENT_SLAVE表示是用于主从复制的客户端, CLIENT_BLOCKED表示客户端是阻塞状态    !(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_BLOCKED)) &&// I/O 线程操作为 IDLE(空闲)说明执行到这的是主线程执行读取事件,只需要把客户端放入server.clients_pending_read, io_threads_op == IO_THREADS_OP_IDLE) {listAddNodeHead(server.clients_pending_read,c);// 如果满足条件,将客户端添加到待读取客户端列表的头部c->pending_read_list_node = listFirst(server.clients_pending_read);return 1;//表示此客户端的放入了待读取客户端列表,主线程应该返回,不再执行后续的操作} else {return 0;//表示此客户端没有放入待读取客户端列表,可以由主线程继续执行后续的操作}
}

2、开启多线程后,主线程执行完命令把数据写回客户端输出缓冲区并且把此客户端写入等待输出客户端列表

在前面的文章中,知道从输出(回复)缓冲区到socket是通过addReply函数

//将结果返回到回复缓冲区
void addReply(client *c, robj *obj) {if (prepareClientToWrite(c) != C_OK) return;//主线程将数据写入输出缓冲区if (sdsEncodedObject(obj)) {_addReplyToBufferOrList(c,obj->ptr,sdslen(obj->ptr));} else if (obj->encoding == OBJ_ENCODING_INT) {/* For integer encoded strings we just convert it into a string* using our optimized function, and attach the resulting string* to the output buffer. */char buf[32];size_t len = ll2string(buf,sizeof(buf),(long)obj->ptr);_addReplyToBufferOrList(c,buf,len);} else {serverPanic("Wrong obj->encoding in addReply()");}
}
int prepareClientToWrite(client *c) {  /* Schedule the client to write the output buffers to the socket, unless* it should already be setup to do so (it has already pending data).** If CLIENT_PENDING_READ is set, we're in an IO thread and should* not put the client in pending write queue. Instead, it will be* done by handleClientsWithPendingReadsUsingThreads() upon return.*///调用了clientHasPendingReplies函数判断输出缓冲区是否有还有数据等待写回到客户端//如果当前客户端输出缓冲区没有数据了,并且线程还处于空闲状态,则执行putClientInPendingWriteQueue把当前客户端放入待输出客户端列表if (!clientHasPendingReplies(c) && io_threads_op == IO_THREADS_OP_IDLE)putClientInPendingWriteQueue(c);/* Authorize the caller to queue in the output buffer of this client. */return C_OK;
}
void putClientInPendingWriteQueue(client *c) {/* Schedule the client to write the output buffers to the socket only* if not already done and, for slaves, if the slave can actually receive* writes at this stage. */if (!(c->flags & CLIENT_PENDING_WRITE) && //客户端标识不是CLIENT_PENDING_WRITE(c->replstate == REPL_STATE_NONE || //(客户端未在进行主从复制||(客户端是主从复制的从节点,但全量复制的 RDB 文件已经传输完成,客户端可以接收请求))(c->replstate == SLAVE_STATE_ONLINE && !c->repl_start_cmd_stream_on_ack))){/* Here instead of installing the write handler, we just flag the* client and put it into a list of clients that have something* to write to the socket. This way before re-entering the event* loop, we can try to directly write to the client sockets avoiding* a system call. We'll only really install the write handler if* we'll not be able to write the whole reply at once. *///将客户端的表示设置为等待输出c->flags |= CLIENT_PENDING_WRITE;//把当前客户端加入server.clients_pending_writelistLinkNodeHead(server.clients_pending_write, &c->clients_pending_write_node);}
}

四、给两个等待客户端端列表分配多线程执行

通过以前的文章,知道redis集群的事件都是通过aeMain循环执行的,其中执行函数aeProcessEventsbeforeSleep会给多线程分配要处理的客户端

void aeMain(aeEventLoop *eventLoop) {eventLoop->stop = 0;while (!eventLoop->stop) {aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_BEFORE_SLEEP|AE_CALL_AFTER_SLEEP);}
}
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{//省略....if (eventLoop->beforesleep != NULL && (flags & AE_CALL_BEFORE_SLEEP))eventLoop->beforesleep(eventLoop);//省略....
}
void beforeSleep(struct aeEventLoop *eventLoop) {/* We should handle pending reads clients ASAP after event loop.我们应该在事件循环后尽快处理挂起的读取客户端 */// 调用了handleClientsWithPendingReadsUsingThreads为等待读取客户端列表中的客户端分配线程执行 */handleClientsWithPendingReadsUsingThreads();/* Handle writes with pending output buffers. *///处理具有挂起输出缓冲区的写入。//调用了handleClientsWithPendingWritesUsingThreads为等待输出客户端列表的分配线程handleClientsWithPendingWritesUsingThreads();
}

1、给等待写入客户端列表分配线程后,多线程只执行到把数据从socket写入到客户端输出缓冲区,不会执行客户端输入缓冲区命令,当多线程都执行完毕后,由主线程执行各个客户端输入缓冲区的命令

在分配线程后,到全部线程都执行完从socket写入到客户端缓冲区之前,io_threads_op都是read状态,不是空闲状态,多线程之后完毕后,io_threads_op改为空闲,由主线程执行命令,这些环节都在beforeSleep函数中

int handleClientsWithPendingReadsUsingThreads(void) {//如果没有开启多线程或者不是读取线程状态,直接返回if (!server.io_threads_active || !server.io_threads_do_reads) return 0;//    从server.clients_pending_read获取等待读取操作的客户端int processed = listLength(server.clients_pending_read);//如果没有等待读取操作的客户端,直接返回if (processed == 0) return 0;/* Distribute the clients across N different lists. */listIter li;listNode *ln;//获得延迟读取操作客户端的迭代列表lilistRewind(server.clients_pending_read,&li);int item_id = 0;//下面根据取余把待读取客户端列表分配线程,包括主线程while((ln = listNext(&li))) {//获取每一个待处理的客户端clientclient *c = listNodeValue(ln);//item_id表示每个客户端的序号,从0开始,int target_id = item_id % server.io_threads_num;listAddNodeTail(io_threads_list[target_id],c);//每处理一个客户端就增1item_id++;}/* Give the start condition to the waiting threads, by setting the* start condition atomic var. *///设置多线程的状态为读取状态io_threads_op = IO_THREADS_OP_READ;for (int j = 1; j < server.io_threads_num; j++) {int count = listLength(io_threads_list[j]);setIOPendingCount(j, count);}/* Also use the main thread to process a slice of clients. *///因为其他线程有IOThreadMain执行,,而主线程是排除在外的,所以这里单独针对主线程和它可以操作的客户端做readlistRewind(io_threads_list[0],&li);while((ln = listNext(&li))) {client *c = listNodeValue(ln);//这个是从socket到客户端读取缓冲区的执行函数,并且注意这时候io_threads_op不是空闲,是read状态readQueryFromClient(c->conn);}listEmpty(io_threads_list[0]);/* Wait for all the other threads to end their work. *///等待所有其他线程结束其工作。while(1) {unsigned long pending = 0;for (int j = 1; j < server.io_threads_num; j++)pending += getIOPendingCount(j);if (pending == 0) break;}//设置多线程的状态为空闲io_threads_op = IO_THREADS_OP_IDLE;/* Run the list of clients again to process the new buffers. *///    /* 再次判断server.clients_pending_read是否有待处理的客户端*///主线程执行各个客户端缓冲区的命令while(listLength(server.clients_pending_read)) {ln = listFirst(server.clients_pending_read);client *c = listNodeValue(ln);// 删除节点listDelNode(server.clients_pending_read,ln);c->pending_read_list_node = NULL;//省略。。。。。。// 这里解析命令并执行,请看下面的(2)介绍if (processPendingCommandAndInputBuffer(c) == C_ERR) {/* If the client is no longer valid, we avoid* processing the client later. So we just go* to the next. */continue;}/* We may have pending replies if a thread readQueryFromClient() produced* replies and did not put the client in pending write queue (it can't).*/if (!(c->flags & CLIENT_PENDING_WRITE) && clientHasPendingReplies(c))putClientInPendingWriteQueue(c);}/* Update processed count on server */server.stat_io_reads_processed += processed;return processed;
}

(1)多线程只执行到从socket到客户端的输入缓冲区

readQueryFromClient->processInputBuffer

void readQueryFromClient(connection *conn) {//省略。。。if (processInputBuffer(c) == C_ERR)c = NULL;//省略。。。。      
}
int processInputBuffer(client *c) {/* Multibulk processing could see a <= 0 length. */if (c->argc == 0) {resetClient(c);} else {/* If we are in the context of an I/O thread, we can't really* execute the command here. All we can do is to flag the client* as one that needs to process the command. */if (io_threads_op != IO_THREADS_OP_IDLE) {//多线程执行读取期间,io_threads_op的状态是read,所以直接break,后面的processCommandAndResetClient无法执行,serverAssert(io_threads_op == IO_THREADS_OP_READ);c->flags |= CLIENT_PENDING_COMMAND;break;}//执行命令/* We are finally ready to execute the command. */if (processCommandAndResetClient(c) == C_ERR) {/* If the client is no longer valid, we avoid exiting this* loop and trimming the client buffer later. So we return* ASAP in that case. */return C_ERR;}}}      

(2) 多线程之后完毕后,主线程执行客户端的缓冲区的命令

上面多线程读取socket到客户端输入缓冲区后之后,即handleClientsWithPendingReadsUsingThreads 最后面,主线程while遍历了客户端,执行processPendingCommandAndInputBuffer

int processPendingCommandAndInputBuffer(client *c) {/* Notice, this code is also called from 'processUnblockedClients'.* But in case of a module blocked client (see RM_Call 'K' flag) we do not reach this code path.* So whenever we change the code here we need to consider if we need this change on module* blocked client as well */if (c->flags & CLIENT_PENDING_COMMAND) {c->flags &= ~CLIENT_PENDING_COMMAND;if (processCommandAndResetClient(c) == C_ERR) {return C_ERR;}}/* Now process client if it has more data in it's buffer.** Note: when a master client steps into this function,* it can always satisfy this condition, because its querybuf* contains data not applied. */if (c->querybuf && sdslen(c->querybuf) > 0) {return processInputBuffer(c);}return C_OK;
}

processCommandAndResetClient方法就是命令执行函数

2、给等待输出客户端列表分配线程后,多线程(包含主线程)开始执行把从客户端输出缓冲区写入到socket,如果没有写完,还会注册文件事件,让aeMain执行,

在分配线程后,到全部线程都执行完从socket写入到客户端缓冲区之前,io_threads_op都是writer状态,不是空闲状态

int handleClientsWithPendingWritesUsingThreads(void) {//如果等待输出客户端列表没有,则直接返回int processed = listLength(server.clients_pending_write);if (processed == 0) return 0; /* Return ASAP if there are no clients. *//* If I/O threads are disabled or we have few clients to serve, don't* use I/O threads, but the boring synchronous code. *///没有开启多线程,由主线程直接执行handleClientsWithPendingWrites返回if (server.io_threads_num == 1 || stopThreadedIOIfNeeded()) {return handleClientsWithPendingWrites();}/* Start threads if needed. */if (!server.io_threads_active) startThreadedIO();/* Distribute the clients across N different lists. */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;/* Remove clients from the list of pending writes since* they are going to be closed ASAP. */if (c->flags & CLIENT_CLOSE_ASAP) {listUnlinkNode(server.clients_pending_write, ln);continue;}/* Since all replicas and replication backlog use global replication* buffer, to guarantee data accessing thread safe, we must put all* replicas client into io_threads_list[0] i.e. main thread handles* sending the output buffer of all replicas. */if (getClientType(c) == CLIENT_TYPE_SLAVE) {listAddNodeTail(io_threads_list[0],c);continue;}//根据线程数取余,分配线程int target_id = item_id % server.io_threads_num;listAddNodeTail(io_threads_list[target_id],c);item_id++;}/* Give the start condition to the waiting threads, by setting the* start condition atomic var. *///设置多线程状态为写入状态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);}//0是主线程,主线程单独遍历它所获取的待写入客户端列表,执行writeToClient//因为非主线程,有IOThreadMain函数/* Also use the main thread to process a slice of clients. */listRewind(io_threads_list[0],&li);while((ln = listNext(&li))) {client *c = listNodeValue(ln);//里面有_writeToClient函数执行客户端输出缓冲区到socketwriteToClient(c,0);}listEmpty(io_threads_list[0]);//这个是主线程等待其他线程都写完/* Wait for all the other threads to end their work. */while(1) {unsigned long pending = 0;for (int j = 1; j < server.io_threads_num; j++)pending += getIOPendingCount(j);if (pending == 0) break;}//多线程状态设置为空闲io_threads_op = IO_THREADS_OP_IDLE;/* Run the list of clients again to install the write handler where* needed. */// 再次获取server.clients_pending_write所有待写的客户端listRewind(server.clients_pending_write,&li);while((ln = listNext(&li))) {client *c = listNodeValue(ln);/* Update the client in the mem usage after we're done processing it in the io-threads */updateClientMemUsageAndBucket(c);/* Install the write handler if there are pending writes in some* of the clients. *///如果缓冲区数据未全部写回调用installClientWriteHandler注册可写事件,回调函数为sendReplyToClientif (clientHasPendingReplies(c)) {installClientWriteHandler(c);}}while(listLength(server.clients_pending_write) > 0) {listUnlinkNode(server.clients_pending_write, server.clients_pending_write->head);}/* Update processed count on server */server.stat_io_writes_processed += processed;return processed;
}

相关文章:

Redis 6.5 服务端开启多线程源码

redis支持开启多线程&#xff0c;只有从socket到读取缓冲区和从输出缓冲区到socket这两段过程是多线程&#xff0c;而命令的执行还是单线程&#xff0c;并且是由主线程执行 借鉴&#xff1a;【Redis】事件驱动框架源码分析&#xff08;多线程&#xff09; 一、main启动时初始化…...

嵌入式面试笔试刷题(day6)

文章目录 前言一、进程和线程的区别二、共享内存的原理三、中断有传参和返回值吗四、串口数据帧格式五、进程通信有几种&#xff0c;哪几种需要借助内核1.方式2.需要借助内核的 六、flash有哪几种类型七、指针的本质是什么八、指针和数组的区别九、使用宏定义交换变量不能使用中…...

24考研数据结构-第五章:树与二叉树

目录 第五章&#xff1a;树5.1树的基本概念5.1.1树的定义5.1.2 基本术语5.1.3 树的性质 5.2二叉树的概念5.2.1 二叉树的定义与特性5.2.2 几种特殊的二叉树5.2.3 二叉树的性质5.2.4 完全二叉树的性质5.2.5 二叉树的存储结构1. 顺序存储重要的基本操作非完全二叉树2. 链式存储逆向…...

构建稳健的微服务架构:关键的微服务设计原则和最佳实践

在现代软件开发中&#xff0c;微服务架构正逐渐成为构建复杂应用程序的首选方法之一。微服务架构的核心理念是将应用程序划分为一系列小型、自治的服务&#xff0c;每个服务专注于一个特定的业务功能。然而&#xff0c;要实现一个稳健的微服务架构并不仅仅是将功能拆分成微服务…...

消息队列常见问题(1)-如何保障不丢消息

目录 1. 为什么消息队列会丢消息&#xff1f; 2. 怎么保障消息可靠传递&#xff1f; 2.1 生产者不丢消息 2.2 服务端不丢消息 2.3 消费者不丢消息 3. 消息丢失如何快速止损&#xff1f; 3.1 完善监控 3.2 完善止损工具 1. 为什么消息队列会丢消息&#xff1f; 现在主流…...

Circle of Mistery 2023牛客暑期多校训练营5 B

登录—专业IT笔试面试备考平台_牛客网 题目大意&#xff1a;给出一个n个数的数组a&#xff0c;求一个排列&#xff0c;使其形成的其中一个置换环上的数的和>k&#xff0c;并使产生的逆序对数量最少 1<n<1e3;-1e6<k<1e6;-1e6<ai<1e6 tips:关于置换环是什…...

VC9、VC10、VC11等等各对应什么版本的Visual Studio,以及含义

文章目录 1、_MSC_VER 定义编译器的版本2、示例 1、_MSC_VER 定义编译器的版本 MS VC 15.0 _MSC_VER 1910 (Visual Studio 2017) MS VC 14.0 _MSC_VER 1900 (Visual Studio 2015) MS VC 12.0 _MSC_VER 1800 (VisualStudio 2013) MS VC 11.0 _MSC_VER 1700 (VisualStudio…...

两数相加 LeetCode热题100

题目 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这两个数都不会…...

Python基础 P2数字类型与优先级进阶练习

文章目录 Python基础 P2数字类型与优先级进阶练习1.闰年判断器2.进制转换及求和3.单位转换 Python基础 P2数字类型与优先级进阶练习 1.闰年判断器 简介 对于闰年的判断就是判断输入的内容类型是否符合要求&#xff0c;然后通过逻辑判断和运算得出该年份是否为闰年 举个栗子 …...

CAPL通过继电器实现CAN容错性自动化测试

系列文章目录 文章目录 系列文章目录前言一、环境搭建1.硬件环境2.软件环境3.继电器线路连接图:二、容错性测试方法1.CAN_H与CAN_L短路2.CAN_H与GND短路3.CAN_L与GND短路4.CAN_H与电源短路5.CAN_L与电源短路6.CAN_H断路7.CAN_L断路三、CAPL自动化测试1.测试用例目录2.测试报告…...

elasticsearch 配置用户名和密码

无密码的其他配置项在&#xff1a;https://blog.csdn.net/Xeon_CC/article/details/132064295 elasticsearch.yml配置文件&#xff1a; xpack.security.enabled: true xpack.security.http.ssl.enabled: true xpack.security.http.ssl.keystore.path: /path/to/elastic-certi…...

侯捷 C++面向对象编程笔记——9 复合 委托

9 复合 委托 9.1 Composition 复合 类似于c中结构里有结构——class里有class deque 是一个已经存在的功能很多的类&#xff08;两头进出的队列&#xff09;&#xff1b;利用deque的功能来实现queue的多种操作 该例只是复合的一种情况——设计模式 Adapter 9.1.1 复合下的构造…...

状态模式——对象状态及其转换

1、简介 1.1、概述 在软件系统中&#xff0c;有些对象也像水一样具有多种状态&#xff0c;这些状态在某些情况下能够相互转换&#xff0c;而且对象在不同的状态下也将具有不同的行为。为了更好地对这些具有多种状态的对象进行设计&#xff0c;可以使用一种被称为状态模式的设…...

Linux一阶段复习

Linux之父是林纳斯本纳第克特托瓦兹 Apache发布目录&#xff1a;/var/www/html nginx发布目录&#xff1a;/usr/share/nginx/html/ 配置dns的文件 &#xff1a; /etc/resolv.conf nginx的配置文件&#xff1a;/etc/nginx/ yum源的配置文件&#xff1a;/etc/yum.repos.d/ …...

宝塔Linux面板怎么升级?升级命令及失败解决方法

宝塔Linux面板怎么升级到新版本&#xff1f;root账号ssh登录到云服务器后&#xff0c;执行宝塔Linux面板升级命令即可搞定&#xff0c;新手站长分享宝塔Linux面板升级命令&#xff1a; 宝塔面板升级到新版本 1、使用root账号ssh登录到云服务器上 ssh root你的云服务器ip地址…...

前端面试的性能优化部分(6)每天10个小知识点

目录 系列文章目录前端面试的性能优化部分&#xff08;1&#xff09;每天10个小知识点前端面试的性能优化部分&#xff08;2&#xff09;每天10个小知识点前端面试的性能优化部分&#xff08;3&#xff09;每天10个小知识点前端面试的性能优化部分&#xff08;4&#xff09;每天…...

2023年 Java 面试八股文(20w字)

目录 第一章-Java基础篇 1、你是怎样理解OOP面向对象 难度系数&#xff1a;⭐ 2、重载与重写区别 难度系数&#xff1a;⭐ 3、接口与抽象类的区别 难度系数&#xff1a;⭐ 4、深拷贝与浅拷贝的理解 难度系数&#xff1a;⭐ 5、sleep和wait区别 难度系数&a…...

银河麒麟服务器ky10-server在线一键安装docker

脚本代码 # ---------------在线安装docker------------------- yum install docker -y # 修改docker拉取源为国内 rm -rf /etc/docker mkdir -p /etc/docker touch /etc/docker/daemon.json cat >/etc/docker/daemon.json<<EOF{"registry-mirrors": [&q…...

spring boot中web容器配置

web容器配置 spring boot 默认的web容器是 tomcat&#xff0c;如果需要换成其他的 web 容器&#xff0c;可以如下配置。 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><!-- 默…...

DNSlog注入(利用DNSlog平台将SQL盲注变成回显注入)

前言什么是UNC什么是DNSlog注入DNSlog注入的条件防止DNSlog注入的几个措施 sqli-labs试验 前言 前几天面试的时候&#xff0c;面试官问我知不知道OOB&#xff08;带外数据&#xff09;。 当时我蒙了&#xff0c;确实没听说过这个东西&#xff0c;然后面试官告诉我原来dnslog注入…...

网络六边形受到攻击

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 抽象 现代智能交通系统 &#xff08;ITS&#xff09; 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 &#xff08;…...

【论文笔记】若干矿井粉尘检测算法概述

总的来说&#xff0c;传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度&#xff0c;通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...

RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程

本文较长&#xff0c;建议点赞收藏&#xff0c;以免遗失。更多AI大模型应用开发学习视频及资料&#xff0c;尽在聚客AI学院。 本文全面剖析RNN核心原理&#xff0c;深入讲解梯度消失/爆炸问题&#xff0c;并通过LSTM/GRU结构实现解决方案&#xff0c;提供时间序列预测和文本生成…...

技术栈RabbitMq的介绍和使用

目录 1. 什么是消息队列&#xff1f;2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...

使用Spring AI和MCP协议构建图片搜索服务

目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式&#xff08;本地调用&#xff09; SSE模式&#xff08;远程调用&#xff09; 4. 注册工具提…...

动态 Web 开发技术入门篇

一、HTTP 协议核心 1.1 HTTP 基础 协议全称 &#xff1a;HyperText Transfer Protocol&#xff08;超文本传输协议&#xff09; 默认端口 &#xff1a;HTTP 使用 80 端口&#xff0c;HTTPS 使用 443 端口。 请求方法 &#xff1a; GET &#xff1a;用于获取资源&#xff0c;…...

零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程

STM32F1 本教程使用零知标准板&#xff08;STM32F103RBT6&#xff09;通过I2C驱动ICM20948九轴传感器&#xff0c;实现姿态解算&#xff0c;并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化&#xff0c;适合嵌入式及物联网开发者。在基础驱动上新增…...

【LeetCode】算法详解#6 ---除自身以外数组的乘积

1.题目介绍 给定一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c;且在 O…...

深度学习之模型压缩三驾马车:模型剪枝、模型量化、知识蒸馏

一、引言 在深度学习中&#xff0c;我们训练出的神经网络往往非常庞大&#xff08;比如像 ResNet、YOLOv8、Vision Transformer&#xff09;&#xff0c;虽然精度很高&#xff0c;但“太重”了&#xff0c;运行起来很慢&#xff0c;占用内存大&#xff0c;不适合部署到手机、摄…...

MyBatis中关于缓存的理解

MyBatis缓存 MyBatis系统当中默认定义两级缓存&#xff1a;一级缓存、二级缓存 默认情况下&#xff0c;只有一级缓存开启&#xff08;sqlSession级别的缓存&#xff09;二级缓存需要手动开启配置&#xff0c;需要局域namespace级别的缓存 一级缓存&#xff08;本地缓存&#…...