《redis4.0 通信模塊源碼分析(一)》

? ? ???

【redis導讀】redis作為一款高性能的內存數據庫,面試服務端開發,redis是繞不開的話題,如果想提升自己的網絡編程的水平和技巧,redis這款優秀的開源軟件是很值得大家去分析和研究的。

? ??

? ? ? 筆者從大學畢業一直有分析redis源碼的想法,但由于各種原因,一直沒有付諸行動,今天抽空把redis4.0的源碼做了一次深層次的剖析,redis作為一款高效的、支持高并發的內存型數據庫,相信很多同學認為redis采用了非常復雜的網絡通信架構,但實則不然!redis之所以性能高,redis4.0采用了單線程的模式(redis6.0不再是單線程模式),有效地避免了線程切換和同步所帶的性能開銷;redis鍵值對全部存儲在內存中,redis自實現了一套高效的內存管理機制,數據的存取都是直接訪問內存,無需進行磁盤IO訪問。

1、前期準備工作

? ??centos的終端上運行:

wget http://download.redis.io/releases/redis-4.0.11.tar.gztar -zxvf redis-4.0.11.tar.gzcd?redis-4.0.11make -j 5

? ? ?編譯redis源碼:

圖片

? ? ??gdb調試redis-server:

?gdb redis-server?r

圖片

? ?在redis編譯目錄下,再啟一個終端,運行如下指令,把redis-client運行起來:

gdb redis-clirset?hello redis

圖片

? ? 這樣就完成了redis的前期準備工作,可以高效地往redis-server中更新鍵值對,好那接下來看看redis-server關于服務端源碼的剖析。

2、調試源碼

? ? ??redis-server也是作為一個獨立的進程,既然是獨立的進程,那么程序肯定有入口點,也即是main函數入口,全局搜索了下redis的源碼,可以看到server.c中有main函數有入口。

圖片

int?main(int?argc,?char?**argv)?{? ? ......? ??//初始化服務端? ? initServer();? ? //設置一些回調函數? ? aeSetBeforeSleepProc(server.el, beforeSleep);? ? aeSetAfterSleepProc(server.el, afterSleep);? ? //aeMain開啟事件循環? ? aeMain(server.el);? ? ......? ? aeDeleteEventLoop(server.el);? ? return 0;}

? ?以上是server.c中main函數的主要執行流,只有一個主線程,初始化服務,設置回調,開始事件循環。那逐步開始拆解,先看看initServer()的執行流。

? ? 備注:initServer()接口中很多細節值得大家去學習,也是編寫服務端程序容易被遺漏的細節

/* Global vars */
struct redisServer server; /* Server global state */void setupSignalHandlers(void) {struct sigaction act;/* When the SA_SIGINFO flag is set in sa_flags then sa_sigaction is used.* Otherwise, sa_handler is used. */sigemptyset(&act.sa_mask);act.sa_flags = 0;act.sa_handler = sigShutdownHandler;sigaction(SIGTERM, &act, NULL);sigaction(SIGINT, &act, NULL);......return;
}void initServer(void) 
{int j;/*忽略SIGHUP、SIGPIPE信號,否則這兩個信號容易把redis進程給掛掉*/signal(SIGHUP, SIG_IGN);signal(SIGPIPE, SIG_IGN);//設置指定信號處理函數。setupSignalHandlers();....../*全局redisServer對象,生命周期和整個進程保持一致redisServer對象保存了事件循環、客戶端隊列等成員變量*/server.pid = getpid();server.current_client = NULL;server.clients = listCreate();server.clients_to_close = listCreate();server.slaves = listCreate();server.monitors = listCreate();//clients_pending_write表示已連接客戶端,但未注冊寫事件的隊列server.clients_pending_write = listCreate();server.slaveseldb = -1; server.unblocked_clients = listCreate();server.ready_keys = listCreate();//還未給回復的客戶端隊列server.clients_waiting_acks = listCreate();server.get_ack_from_slaves = 0;server.clients_paused = 0;server.system_memory_size = zmalloc_get_memory_size();createSharedObjects();adjustOpenFilesLimit();/*根據配置的參數,給主evetLoop的各成員隊列初始化指定大小的空間比如: 讀、寫回調函數的aeFileEvent隊列typedef struct aeFileEvent {int mask;//可讀、可寫、異常aeFileProc *rfileProc;aeFileProc *wfileProc;void *clientData;}aeFileEvent;*///全局就一個redisServer,一個redisServer對應一個eventLoopserver.el = aeCreateEventLoop(server.maxclients + CONFIG_FDSET_INCR);if (server.el == NULL) {serverLog(LL_WARNING,"Failed creating the event loop. Error message: '%s'",strerror(errno));exit(1);}server.db = zmalloc(sizeof(redisDb) * server.dbnum);//開啟監聽if (server.port != 0 &&listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)exit(1);if (server.unixsocket != NULL) {unlink(server.unixsocket); /* don't care if this fails */server.sofd = anetUnixServer(server.neterr,server.unixsocket,server.unixsocketperm, server.tcp_backlog);if (server.sofd == ANET_ERR) {serverLog(LL_WARNING, "Opening Unix socket: %s", server.neterr);exit(1);}//將socket設置成非阻塞的anetNonBlock(NULL,server.sofd);}......//創建Redis定時器,用于執行定時任務if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {serverPanic("Can't create event loop timers.");exit(1);}/*1、為redisServer監聽套接字設置連接建立成功回調函數acceptTcpHandler,只關注可讀事件,監聽套接字產生可讀事件,說明連接建立成功。2、將監聽socket綁定到IO復用模型上面去*/for (j = 0; j < server.ipfd_count; j++) {if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,acceptTcpHandler, NULL) == AE_ERR){serverPanic("Unrecoverable error creating server.ipfd file event.");}}if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd, AE_READABLE,acceptUnixHandler,NULL) == AE_ERR) serverPanic("Unrecoverable error creating server.sofd file event.");/* 創建一個管道,用于主動喚醒被epoll_wait掛起的eventLoop */if (aeCreateFileEvent(server.el, server.module_blocked_pipe[0], AE_READABLE,moduleBlockedClientPipeReadable, NULL) == AE_ERR) {serverPanic("Error registering the readable event for the module ""blocked clients subsystem.");}......
}

??? ?基于上述的主流程,我們進一步剖析,如何將監聽socket綁定到IO多路復用模型上?進一步剖析aeCreateFileEvent接口。

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData)
{if (fd >= eventLoop->setsize) {errno = ERANGE;return AE_ERR;}aeFileEvent *fe = &eventLoop->events[fd];if (aeApiAddEvent(eventLoop, fd, mask) == -1)return AE_ERR;fe->mask |= mask;if (mask & AE_READABLE) fe->rfileProc = proc;if (mask & AE_WRITABLE) fe->wfileProc = proc;fe->clientData = clientData;if (fd > eventLoop->maxfd)eventLoop->maxfd = fd;return AE_OK;
}static int aeApiAddEvent(aeEventLoop *eventLoop,int fd, int mask) {aeApiState *state = eventLoop->apidata;struct epoll_event ee = {0};int op = eventLoop->events[fd].mask == AE_NONE ?EPOLL_CTL_ADD : EPOLL_CTL_MOD;ee.events = 0;mask |= eventLoop->events[fd].mask; /* Merge old events */if (mask & AE_READABLE)ee.events |= EPOLLIN;if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;ee.data.fd = fd;//從這里看redis使用epoll模型,將fd綁定到epfd上if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;return 0;
}


? ? ? 假設epoll模型檢測到監聽套接字有可讀事件產生,那主Loop的勢必從epoll_wait接口返回,再根據事件類型,轉調我們提前設置的回調函數acceptTcpHandler中來。

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {int cport, cfd, max = MAX_ACCEPTS_PER_CALL;char cip[NET_IP_STR_LEN];UNUSED(el);UNUSED(mask);UNUSED(privdata);while(max--) {//調用accept接口,生成一個客戶端套接字cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);if (cfd == ANET_ERR) {if (errno != EWOULDBLOCK)serverLog(LL_WARNING,"Accepting client connection: %s", server.neterr);return;}serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);//acceptCommonHandler(cfd,0,cip);}
}#define MAX_ACCEPTS_PER_CALL 1000
static void acceptCommonHandler(int fd, int flags, char *ip) {client *c;//創建cif ((c = createClient(fd)) == NULL) {serverLog(LL_WARNING,"Error registering fd event for the new client: %s (fd=%d)",strerror(errno),fd);close(fd); /* May be already closed, just ignore errors */return;}......
}//以客戶端套接字創建一個client對象
client *createClient(int fd) {client *c = zmalloc(sizeof(client));if (fd != -1) {//將客戶端套接字設置成非阻塞的anetNonBlock(NULL,fd);//關閉nagel算法anetEnableTcpNoDelay(NULL,fd);//設置TCP鏈接保活機制if (server.tcpkeepalive)anetKeepAlive(NULL,fd,server.tcpkeepalive);/*將客戶端套接字綁定到epfd上,同時設置可讀事件回調函數readQueryFromClient*/  if (aeCreateFileEvent(server.el, fd, AE_READABLE,readQueryFromClient, c) == AE_ERR){close(fd);zfree(c);return NULL;}}......
}
 

? ??那接著看客戶端套接字產生了可讀事件,進而主Loop循環會執行到和當前客戶端套接字相關的回調函數中來,一起看下readQueryFromClient的源碼。

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) 
{client *c = (client*)privdata;int nread, readlen;size_t qblen;UNUSED(el);UNUSED(mask);readlen = PROTO_IOBUF_LEN;if (c->reqtype == PROTO_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1&& c->bulklen >= PROTO_MBULK_BIG_ARG){ssize_t remaining = (size_t)(c->bulklen+2) - sdslen(c->querybuf);if (remaining < readlen)readlen = remaining;}qblen = sdslen(c->querybuf);if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);nread = read(fd, c->querybuf + qblen, readlen);if (nread == -1) {if (errno == EAGAIN) {//說明當前接收緩沖區不夠,沒法讀到最新的數據return;} else {//那說明真的出錯了serverLog(LL_VERBOSE, "Reading from client: %s",strerror(errno));freeClient(c);return;}} else if (nread == 0) {serverLog(LL_VERBOSE, "Client closed connection");freeClient(c);return;} else if (c->flags & CLIENT_MASTER){c->pending_querybuf = sdscatlen(c->pending_querybuf, c->querybuf + qblen, nread);}......if (!(c->flags & CLIENT_MASTER)) {processInputBuffer(c);} else {size_t prev_offset = c->reploff;processInputBuffer(c);size_t applied = c->reploff - prev_offset;if (applied) {replicationFeedSlavesFromMasterStream(server.slaves, c->pending_querybuf, applied);sdsrange(c->pending_querybuf, applied, -1);}}
}
 

? ??processInputBuffer 判斷接收到的字符串是不是以星號( * )開頭,如果以*開頭,設置 client 對象的 reqtype 字段值為 PROTO_REQ_MULTIBULK ,接著調用 processMultibulkBuffer 函數繼續處理剩余的字符串。處理后的字符串被解析成 redis 命令,如果是具體的命令,那么redis會按照指定的規則去執行。

? ? 既然提到指令command,那么processInputBuffer 接口中肯定有和指令command處理相關的接口。

int processCommand(client *c) {//如果是quit指令,那么給客戶端回應一個ok的應答replayif (!strcasecmp(c->argv[0]->ptr,"quit")) {addReply(c,shared.ok);c->flags |= CLIENT_CLOSE_AFTER_REPLY;return C_ERR;}//查找指令,執行對應的指令,出錯了,給客戶端回應一個錯誤信息c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);if (!c->cmd) {flagTransaction(c);sds args = sdsempty();int i;for (i=1; i < c->argc && sdslen(args) < 128; i++)args = sdscatprintf(args, "`%.*s`, ", 128-(int)sdslen(args), (char*)c->argv[i]->ptr);addReplyErrorFormat(c,"unknown command `%s`, with args beginning with: %s",(char*)c->argv[0]->ptr, args);sdsfree(args);return C_OK;} else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||(c->argc < -c->cmd->arity)) {flagTransaction(c);addReplyErrorFormat(c,"wrong number of arguments for '%s' command",c->cmd->name);return C_OK;}......
}
 

? 那繼續看看addReply接口:
?

void addReply(client *c, robj *obj) {if (prepareClientToWrite(c) != C_OK) return;if (sdsEncodedObject(obj)) {if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != C_OK)_addReplyObjectToList(c,obj);} else if (obj->encoding == OBJ_ENCODING_INT) {......} else {serverPanic("Wrong obj->encoding in addReply()");}
}
 

? ? 繼續看prepareClientToWrite接口:

int prepareClientToWrite(client *c) {if (c->flags & (CLIENT_LUA|CLIENT_MODULE)) return C_OK;if (c->flags & (CLIENT_REPLY_OFF|CLIENT_REPLY_SKIP)) return C_ERR;if ((c->flags & CLIENT_MASTER) &&!(c->flags & CLIENT_MASTER_FORCE_REPLY)) return C_ERR;if (c->fd <= 0) return C_ERR; if (!clientHasPendingReplies(c) &&!(c->flags & CLIENT_PENDING_WRITE) &&(c->replstate == REPL_STATE_NONE ||(c->replstate == SLAVE_STATE_ONLINE && !c->repl_put_online_on_ack))){/*如果當前client沒有CLIENT_PENDING_WRITE標記而且沒有暫存的數據要發送,那么給它設置個CLIENT_PENDING_WRITE同時將當前client添加到redisServer的clients_pending_write鏈表中去*/  c->flags |= CLIENT_PENDING_WRITE;listAddNodeHead(server.clients_pending_write, c);}return C_OK;
}

? ? ? 還有接口_addReplyToBuffer:

/*最重要的一步,將客戶端請求command執行的結果添加到cliet對應的buf緩沖區中去。
*/  
int _addReplyToBuffer(client *c, const char *s, size_t len) 
{size_t available = sizeof(c->buf) - c->bufpos;if (c->flags & CLIENT_CLOSE_AFTER_REPLY) return C_OK;/*如果client對應的replay鏈表長度大于0,那么將該應答指令添加到replay鏈表中去*/     if (listLength(c->reply) > 0) return C_ERR;if (len > available) return C_ERR;memcpy(c->buf + c->bufpos, s, len);c->bufpos += len;return C_OK;
}//_addReplyToBuffer返回C_ERR,那將replay添加到replay鏈表
if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != C_OK)_addReplyObjectToList(c, obj);
 

? ?redis4.0最核心的部分就是這個主Loop循環:

void aeMain(aeEventLoop *eventLoop) {eventLoop->stop = 0;while (!eventLoop->stop) {if (eventLoop->beforesleep != NULL)eventLoop->beforesleep(eventLoop);aeProcessEvents(eventLoop, AE_ALL_EVENTS | AE_CALL_AFTER_SLEEP);}
}
 

?? ? 每次循環都會執行下beforesleep接口,beforesleep接口主要做了啥呢,可以看看beforesleep接口的實現:

void beforeSleep(struct aeEventLoop *eventLoop) {UNUSED(eventLoop);/* Handle writes with pending output buffers. */handleClientsWithPendingWrites();......
}int handleClientsWithPendingWrites(void) {listIter li;listNode *ln;int processed = listLength(server.clients_pending_write);//先處理有數據需要發送的鏈表clients_pending_writelistRewind(server.clients_pending_write, &li);while((ln = listNext(&li))) {client *c = listNodeValue(ln);//注銷掉CLIENT_PENDING_WRITE標記c->flags &= ~CLIENT_PENDING_WRITE;listDelNode(server.clients_pending_write,ln);//直接往socket寫數據if (writeToClient(c->fd, c, 0) == C_ERR) continue;//如果當前client對象有需要發送的replayif (clientHasPendingReplies(c)) {int ae_flags = AE_WRITABLE;if (server.aof_state == AOF_ON &&server.aof_fsync == AOF_FSYNC_ALWAYS){ae_flags |= AE_BARRIER;}/*如果tcp窗口太小,那么數據有可能發不出去,將client的fd可寫事件添加到epoll模型上去并注冊可寫回調函數sendReplyToClient*/if (aeCreateFileEvent(server.el, c->fd, ae_flags,sendReplyToClient, c) == AE_ERR){freeClientAsync(c);}}}return processed;
}//sendReplyToClient也是調用writeToClient接口
void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {UNUSED(el);UNUSED(mask);writeToClient(fd,privdata,1);
}
 

? ? ?所以分析了這么多,感覺redis的通信模型就是單線程,外加一個主Loop循環,定義一個全局的redisServer對象,定義多個數據成員鏈表用于管理已連接的client對象集合,需要回復的client對象、有數據需要待發送的client對象集合,epoll模型監聽listenSocket、AcceptSocket可讀事件,客戶端有請求指令發送過來,redisServer解析指令,執行指令,并給client回復執行結果,如果tcp窗口太小,給當前client的fd注冊可寫事件和可寫回調函數sendReplyToClient,待TCP窗口滿足發送數據要求時,sendReplyToClient再執行數據的發送。另外主Loop每次循環時都會主動檢測待回復鏈表replay、待發送鏈表clients_pending_write,如果有數據需要發送給客戶端,逐個遍歷發送。

3、實測驗證

? ? ?在centos7做下實測,我們同時開啟兩個redis-cli,先后給redis-server發送兩個指令

set?hello world

???此時看下redis-server的堆棧以及主線程:

圖片

?? ?redis處理客戶端請求,并不是多線程并發處理,而是循環遍歷去給pending client回復報文,逐一回應。

圖片

? ? ?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/894725.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/894725.shtml
英文地址,請注明出處:http://en.pswp.cn/news/894725.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

開源安全一站式構建!開啟企業開源治理新篇章

在如今信息技術日新月異、飛速發展的數字化時代&#xff0c;開源技術如同一股強勁的東風&#xff0c;為企業創新注入了源源不斷的活力&#xff0c;然而&#xff0c;正如一枚硬幣有正反兩面&#xff0c;開源技術的廣泛應用亦伴隨著不容忽視的挑戰。安全風險如影隨形&#xff0c;…

DeePseek結合PS!批量處理圖片的方法教程

? ? 今天我們來聊聊如何利用deepseek和Photoshop&#xff08;PS&#xff09;實現圖片的批量處理。 傳統上&#xff0c;批量修改圖片尺寸、分辨率等任務往往需要編寫腳本或手動處理&#xff0c;而現在有了AI的輔助&#xff0c;我們可以輕松生成PS腳本&#xff0c;實現自動化處…

13.代理模式(Proxy Pattern)

定義 代理模式&#xff08;Proxy Pattern&#xff09; 是一種結構型設計模式&#xff0c;它通過提供一個代理對象來控制對目標對象的訪問。代理對象作為客戶端與目標對象之間的中介&#xff0c;間接地訪問目標對象的功能。代理模式可以在不改變目標對象的情況下增加一些額外的…

DBeaver連接MySQL提示Access denied for user ‘‘@‘ip‘ (using password: YES)的解決方法

在使用DBeaver連接MySQL數據庫時&#xff0c;如果遇到“Access denied for user ip (using password: YES)”的錯誤提示&#xff0c;說明用戶認證失敗。此問題通常與數據庫用戶權限、配置錯誤或網絡設置有關。本文將詳細介紹解決此問題的步驟。 一、檢查用戶名和密碼 首先&am…

Python進行模型優化與調參

在數據科學與機器學習領域,模型的優化與調參是提高模型性能的重要步驟之一。模型優化可以幫助提高模型的準確性和泛化能力,而合理的調參則能夠充分發揮模型的潛力。這篇教程將重點介紹幾種常用的模型優化與調參方法,特別是超參數調整和正則化技術的應用。這些技術能夠有效地…

Verilog基礎(三):過程

過程(Procedures) - Always塊 – 組合邏輯 (Always blocks – Combinational) 由于數字電路是由電線相連的邏輯門組成的,所以任何電路都可以表示為模塊和賦值語句的某種組合. 然而,有時這不是描述電路最方便的方法. 兩種always block是十分有用的: 組合邏輯: always @(…

2024年12月 Scratch 圖形化(一級)真題解析 中國電子學會全國青少年軟件編程等級考試

202412 Scratch 圖形化&#xff08;一級&#xff09;真題解析 中國電子學會全國青少年軟件編程等級考試 一、單選題(共25題&#xff0c;共50分) 第 1 題 點擊下列哪個按鈕&#xff0c;可以將紅框處的程序放大&#xff1f;&#xff08; &#xff09; A. B. C. D. 標…

C++【深入 STL--list 之 迭代器與反向迭代器】

接前面的手撕list(上)文章&#xff0c;由于本人對于list的了解再一次加深。本文再次對list進行深入的分析與實現。旨在再一次梳理思路&#xff0c;修煉代碼內功。 1、list 基礎架構 list底層為雙向帶頭循環鏈表&#xff0c;問題是如何來搭建這個list類。可以進行下面的考慮&am…

如何打開vscode系統用戶全局配置的settings.json

&#x1f4cc; settings.json 的作用 settings.json 是 Visual Studio Code&#xff08;VS Code&#xff09; 的用戶配置文件&#xff0c;它存儲了 編輯器的個性化設置&#xff0c;包括界面布局、代碼格式化、擴展插件、快捷鍵等&#xff0c;是用戶全局配置&#xff08;影響所有…

wordpress外貿獨立站常用詢盤軟件

LiveChat LiveChat是一家提供實時聊天軟件的公司&#xff0c;幫助企業通過其平臺與客戶進行即時通訊&#xff0c;提高客戶滿意度和忠誠度。他們的產品允許企業在網站、應用程序或電子郵件等多個渠道與客戶互動&#xff0c;從而提升客戶體驗并促進銷售增長。 LiveChat的軟件特…

STM32 ADC模數轉換器

ADC簡介 ADC&#xff08;Analog-Digital Converter&#xff09;模擬-數字轉換器 ADC可以將引腳上連續變化的模擬電壓轉換為內存中存儲的數字變量&#xff0c;建立模擬電路到數字電路的橋梁 12位逐次逼近型ADC&#xff0c;1us轉換時間 輸入電壓范圍&#xff1a;0~3.3V&#xff0…

(2025,LLM,下一 token 預測,擴散微調,L2D,推理增強,可擴展計算)從大語言模型到擴散微調

Large Language Models to Diffusion Finetuning 目錄 1. 概述 2. 研究背景 3. 方法 3.1 用于 LM 微調的高斯擴散 3.2 架構 4. 主要實驗結果 5. 結論 1. 概述 本文提出了一種新的微調方法——LM to Diffusion (L2D)&#xff0c;旨在賦予預訓練的大語言模型&#xff08;…

DeepSeek 與 ChatGPT 對比分析

一、技術背景與研發團隊 ChatGPT 由 OpenAI 開發&#xff0c;自 2015 年 OpenAI 成立以來&#xff0c;經過多年的技術積累和迭代&#xff0c;從 GPT-1 到 GPT-4o&#xff0c;每一次升級都帶來了技術上的突破。OpenAI 擁有雄厚的技術實力和海量的數據、強大的算力支持&#xff…

學習threejs,pvr格式圖片文件貼圖

&#x1f468;??? 主頁&#xff1a; gis分享者 &#x1f468;??? 感謝各位大佬 點贊&#x1f44d; 收藏? 留言&#x1f4dd; 加關注?! &#x1f468;??? 收錄于專欄&#xff1a;threejs gis工程師 文章目錄 一、&#x1f340;前言1.1 ??PVR貼圖1.2 ??THREE.Mesh…

DeepSeek R1技術報告關鍵解析(8/10):DeepSeek-R1 的“aha 時刻”,AI 自主學習的新突破

1. 什么是 AI 的“aha 時刻”&#xff1f; 在強化學習過程中&#xff0c;AI 的推理能力并不是線性增長的&#xff0c;而是會經歷一些關鍵的“頓悟”時刻&#xff0c;研究人員將其稱為“aha 時刻”。 這是 AI 在訓練過程中突然學會了一種新的推理方式&#xff0c;或者能夠主動…

python:遞歸函數與lambda函數

遞歸函數&#xff1a;1.函數內調用自己 2.有一個出口 1.遞歸 一.有出口時 def sum(num):if num1:return 1return numsum(num-1) asum(3) print(a) #num3 3sum(2) #num2 2sum(1) #num1是返回1 #即3sum(2&#xff09;即32sum(1)即321運行結果 6 二.無出口時 def sum(num)…

ABB 3BSE018741R30 帶插頭連接器的電纜

產品ID:3BSE018741R30 ABB型號名稱:PFTL 101/201/PFCL 201 30米 目錄描述:帶插頭連接器的電纜&#xff0c;30米 ABB型號名稱:PFTL 101/201/PFCL 201 30米 核心信用:0.00 原產國:瑞典波蘭 海關稅則號:85389091 框架尺寸:備件 毛重:5公斤 媒體描述:帶插頭連接器的電纜 最小訂購數…

SpringMVC請求

一、RequestMapping注解 RequestMapping注解的作用是建立請求URL和處理方法之間的對應關系 RequestMapping注解可以作用在方法和類上 1. 作用在類上&#xff1a;第一級的訪問目錄 2. 作用在方法上&#xff1a;第二級的訪問目錄 3. 細節&#xff1a;路徑可以不編寫 / 表示應…

VUE的響應性調試:組件調試鉤子、計算屬性調試、偵聽器調試【僅會在開發模式下工作】

文章目錄 引言I 組件調試鉤子調試事件對象的類型定義鉤子II 計算屬性調試例子回調函數說明III 偵聽器調試引言 VUE的響應性調試的使用場景:確切地知道Vue 的響應性系統正在跟蹤什么,或者是什么導致了組件重新渲染。 I 組件調試鉤子 組件調試鉤子僅會在開發模式下工作 調試…

tkvue 入門,像寫html一樣寫tkinter

介紹 沒有官網&#xff0c;只有例子 安裝 像寫vue 一樣寫tkinter 代碼 pip install tkvue作者博客 修改樣式 import tkvue import tkinter.ttk as ttktkvue.configure_tk(theme"clam")class RootDialog(tkvue.Component):template """ <Top…