參考:
https://blog.csdn.net/weixin_37356262/article/details/88827688
https://blog.csdn.net/XingXing_Java/article/details/92626174
為什么Redis是單線程的,並且速度還非常快!
一、前言
近乎所有與Java相關的面試都會問到緩存的問題,基礎一點的會問到什么是“二八定律”、什么是“熱數據和冷數據”,復雜一點的會問到緩存雪崩、緩存穿透、緩存預熱、緩存更新、緩存降級等問題,這些看似不常見的概念,都與我們的緩存服務器相關,一般常用的緩存服務器有Redis、Memcached等,而筆者目前最常用的也只有Redis這一種。
如果你在以前面試的時候還沒有遇到過面試官問你《為什么說Redis是單線程的以及Redis為什么這么快!》,那么你看到這篇文章的時候,你應該覺得是一件很幸運的事情!如果你剛好是一位高逼格的面試官,你也可以拿這道題去面試對面“望穿秋水”般的小伙伴,測試一下他的掌握程度。
好啦!步入正題!我們先探討一下Redis是什么,Redis為什么這么快、然后在探討一下為什么Redis是單線程的?
二、Redis簡介
Redis是一個開源的內存中的數據結構存儲系統,它可以用作:數據庫、緩存和消息中間件。
它支持多種類型的數據結構,如字符串(Strings),散列(Hash),列表(List),集合(Set),有序集合(Sorted Set或者是ZSet)與范圍查詢,Bitmaps,Hyperloglogs 和地理空間(Geospatial)索引半徑查詢。其中常見的數據結構類型有:String、List、Set、Hash、ZSet這5種。
Redis 內置了復制(Replication),LUA腳本(Lua scripting), LRU驅動事件(LRU eviction),事務(Transactions) 和不同級別的磁盤持久化(Persistence),並通過 Redis哨兵(Sentinel)和自動分區(Cluster)提供高可用性(High Availability)。
Redis也提供了持久化的選項,這些選項可以讓用戶將自己的數據保存到磁盤上面進行存儲。根據實際情況,可以每隔一定時間將數據集導出到磁盤(快照),或者追加到命令日志中(AOF只追加文件),他會在執行寫命令時,將被執行的寫命令復制到硬盤里面。您也可以關閉持久化功能,將Redis作為一個高效的網絡的緩存數據功能使用。
Redis不使用表,他的數據庫不會預定義或者強制去要求用戶對Redis存儲的不同數據進行關聯。
數據庫的工作模式按存儲方式可分為:硬盤數據庫和內存數據庫。Redis 將數據儲存在內存里面,讀寫數據的時候都不會受到硬盤 I/O 速度的限制,所以速度極快。
(1)硬盤數據庫的工作模式:
(2)內存數據庫的工作模式:
看完上述的描述,對於一些常見的Redis相關的面試題,是否有所認識了,例如:什么是Redis、Redis常見的數據結構類型有哪些、Redis是如何進行持久化的等。
三、Redis到底有多快
Redis采用的是基於內存的采用的是單進程單線程模型的 KV 數據庫,由C語言編寫,官方提供的數據是可以達到100000+的QPS(每秒內查詢次數)。這個數據不比采用單進程多線程的同樣基於內存的 KV 數據庫 Memcached 差!有興趣的可以參考官方的基准程序測試《How fast is Redis?》(https://redis.io/topics/benchmarks)
橫軸是連接數,縱軸是QPS。此時,這張圖反映了一個數量級,希望大家在面試的時候可以正確的描述出來,不要問你的時候,你回答的數量級相差甚遠!
四、Redis為什么這么快
1、完全基於內存,絕大部分請求是純粹的內存操作,非常快速。數據存在內存中,類似於HashMap,HashMap的優勢就是查找和操作的時間復雜度都是O(1);
2、數據結構簡單,對數據操作也簡單,Redis中的數據結構是專門進行設計的;
3、采用單線程,避免了不必要的上下文切換和競爭條件,也不存在多進程或者多線程導致的切換而消耗 CPU,不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有因為可能出現死鎖而導致的性能消耗;
4、使用多路I/O復用模型,非阻塞IO;
5、使用底層模型不同,它們之間底層實現方式以及與客戶端之間通信的應用協議不一樣,Redis直接自己構建了VM 機制 ,因為一般的系統調用系統函數的話,會浪費一定的時間去移動和請求;
以上幾點都比較好理解,下邊我們針對多路 I/O 復用模型進行簡單的探討:
(1)多路 I/O 復用模型
多路I/O復用模型是利用 select、poll、epoll 可以同時監察多個流的 I/O 事件的能力,在空閑的時候,會把當前線程阻塞掉,當有一個或多個流有 I/O 事件時,就從阻塞態中喚醒,於是程序就會輪詢一遍所有的流(epoll 是只輪詢那些真正發出了事件的流),並且只依次順序的處理就緒的流,這種做法就避免了大量的無用操作。
這里“多路”指的是多個網絡連接,“復用”指的是復用同一個線程。采用多路 I/O 復用技術可以讓單個線程高效的處理多個連接請求(盡量減少網絡 IO 的時間消耗),且 Redis 在內存中操作數據的速度非常快,也就是說內存內的操作不會成為影響Redis性能的瓶頸,主要由以上幾點造就了 Redis 具有很高的吞吐量。
五、那么為什么Redis是單線程的
我們首先要明白,上邊的種種分析,都是為了營造一個Redis很快的氛圍!官方FAQ表示,因為Redis是基於內存的操作,CPU不是Redis的瓶頸,Redis的瓶頸最有可能是機器內存的大小或者網絡帶寬。既然單線程容易實現,而且CPU不會成為瓶頸,那就順理成章地采用單線程的方案了(畢竟采用多線程會有很多麻煩!)。
可以參考:https://redis.io/topics/faq
看到這里,你可能會氣哭!本以為會有什么重大的技術要點才使得Redis使用單線程就可以這么快,沒想到就是一句官方看似糊弄我們的回答!但是,我們已經可以很清楚的解釋了為什么Redis這么快,並且正是由於在單線程模式的情況下已經很快了,就沒有必要在使用多線程了!
但是,我們使用單線程的方式是無法發揮多核CPU 性能,不過我們可以通過在單機開多個Redis 實例來完善!
警告1:這里我們一直在強調的單線程,只是在處理我們的網絡請求的時候只有一個線程來處理,一個正式的Redis Server運行的時候肯定是不止一個線程的,這里需要大家明確的注意一下!例如Redis進行持久化的時候會以子進程或者子線程的方式執行(具體是子線程還是子進程待讀者深入研究);例如我在測試服務器上查看Redis進程,然后找到該進程下的線程:
ps命令的“-T”參數表示顯示線程(Show threads, possibly with SPID column.)“SID”欄表示線程ID,而“CMD”欄則顯示了線程名稱。
警告2:在上圖中FAQ中的最后一段,表述了從Redis 4.0版本開始會支持多線程的方式,但是,只是在某一些操作上進行多線程的操作!所以該篇文章在以后的版本中是否還是單線程的方式需要讀者考證!
六、注意點
1、我們知道Redis是用”單線程-多路復用IO模型”來實現高性能的內存數據服務的,這種機制避免了使用鎖,但是同時這種機制在進行sunion之類的比較耗時的命令時會使redis的並發下降。因為是單一線程,所以同一時刻只有一個操作在進行,所以,耗時的命令會導致並發的下降,不只是讀並發,寫並發也會下降。而單一線程也只能用到一個CPU核心,所以可以在同一個多核的服務器中,可以啟動多個實例,組成master-master或者master-slave的形式,耗時的讀命令可以完全在slave進行。
需要改的redis.conf項:
-
pidfile /var/run/redis/redis_6377.pid #pidfile要加上端口號
-
port 6377 #這個是必須改的
-
logfile /var/log/redis/redis_6377.log #logfile的名稱也加上端口號
-
dbfilename dump_6377.rdb #rdbfile也加上端口號
2、“我們不能任由操作系統負載均衡,因為我們自己更了解自己的程序,所以,我們可以手動地為其分配CPU核,而不會過多地占用CPU,或是讓我們關鍵進程和一堆別的進程擠在一起。”。
CPU 是一個重要的影響因素,由於是單線程模型,Redis 更喜歡大緩存快速 CPU, 而不是多核
在多核 CPU 服務器上面,Redis 的性能還依賴NUMA 配置和處理器綁定位置。最明顯的影響是 redis-benchmark 會隨機使用CPU內核。為了獲得精准的結果,需要使用固定處理器工具(在 Linux 上可以使用 taskset)。最有效的辦法是將客戶端和服務端分離到兩個不同的 CPU 來高校使用三級緩存。
七、擴展
以下也是你應該知道的幾種模型,祝你的面試一臂之力!
1、單進程多線程模型:MySQL、Memcached、Oracle(Windows版本);
2、多進程模型:Oracle(Linux版本);
3、Nginx有兩類進程,一類稱為Master進程(相當於管理進程),另一類稱為Worker進程(實際工作進程)。啟動方式有兩種:
(1)單進程啟動:此時系統中僅有一個進程,該進程既充當Master進程的角色,也充當Worker進程的角色。
(2)多進程啟動:此時系統有且僅有一個Master進程,至少有一個Worker進程工作。
(3)Master進程主要進行一些全局性的初始化工作和管理Worker的工作;事件處理是在Worker中進行的。
Redis為什么是單線程還這么快?
Redis的高並發和快速原因
1.redis是基於內存的,內存的讀寫速度非常快;
2.redis是單線程的,省去了很多上下文切換線程的時間;
3.redis使用多路復用技術,可以處理並發的連接。非阻塞IO 內部實現采用epoll,采用了epoll+自己實現的簡單的事件框架。epoll中的讀、寫、關閉、連接都轉化成了事件,然后利用epoll的多路復用特性,絕不在io上浪費一點時間。
下面重點介紹單線程設計和IO多路復用核心設計快的原因。
為什么Redis是單線程的
1.官方答案
因為Redis是基於內存的操作,CPU不是Redis的瓶頸,Redis的瓶頸最有可能是機器內存的大小或者網絡帶寬。既然單線程容易實現,而且CPU不會成為瓶頸,那就順理成章地采用單線程的方案了。
2.性能指標
關於redis的性能,官方網站也有,普通筆記本輕松處理每秒幾十萬的請求。
3.詳細原因
1)不需要各種鎖的性能消耗
Redis的數據結構並不全是簡單的Key-Value,還有list,hash等復雜的結構,這些結構有可能會進行很細粒度的操作,比如在很長的列表后面添加一個元素,在hash當中添加或者刪除
一個對象。這些操作可能就需要加非常多的鎖,導致的結果是同步開銷大大增加。
總之,在單線程的情況下,就不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有因為可能出現死鎖而導致的性能消耗。
2)單線程多進程集群方案
單線程的威力實際上非常強大,每核心效率也非常高,多線程自然是可以比單線程有更高的性能上限,但是在今天的計算環境中,即使是單機多線程的上限也往往不能滿足需要了,需要進一步摸索的是多服務器集群化的方案,這些方案中多線程的技術照樣是用不上的。
所以單線程、多進程的集群不失為一個時髦的解決方案。
3)CPU消耗
采用單線程,避免了不必要的上下文切換和競爭條件,也不存在多進程或者多線程導致的切換而消耗 CPU。
但是如果CPU成為Redis瓶頸,或者不想讓服務器其他CUP核閑置,那怎么辦?
可以考慮多起幾個Redis進程,Redis是key-value數據庫,不是關系數據庫,數據之間沒有約束。只要客戶端分清哪些key放在哪個Redis進程上就可以了。
Redis單線程的優劣勢
1.單進程單線程優勢
代碼更清晰,處理邏輯更簡單
不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有因為可能出現死鎖而導致的性能消耗
不存在多進程或者多線程導致的切換而消耗CPU
2.單進程單線程弊端
無法發揮多核CPU性能,不過可以通過在單機開多個Redis實例來完善;
IO多路復用技術
redis 采用網絡IO多路復用技術來保證在多連接的時候, 系統的高吞吐量。
多路-指的是多個socket連接,復用-指的是復用一個線程。多路復用主要有三種技術:select,poll,epoll。epoll是最新的也是目前最好的多路復用技術。
這里“多路”指的是多個網絡連接,“復用”指的是復用同一個線程。采用多路
I/O
復用技術可以讓單個線程高效的處理多個連接請求(盡量減少網絡IO的時間消耗),且Redis在內存中操作數據的速度非常快(內存內的操作不會成為這里的性能瓶頸),主要以上兩點造就了Redis具有很高的吞吐量。
Redis高並發快總結
-
Redis是純內存數據庫,一般都是簡單的存取操作,線程占用的時間很多,時間的花費主要集中在IO上,所以讀取速度快。
-
再說一下IO,Redis使用的是非阻塞IO,IO多路復用,使用了單線程來輪詢描述符,將數據庫的開、關、讀、寫都轉換成了事件,減少了線程切換時上下文的切換和競爭。
-
Redis采用了單線程的模型,保證了每個操作的原子性,也減少了線程的上下文切換和競爭。
-
另外,數據結構也幫了不少忙,Redis全程使用hash結構,讀取速度快,還有一些特殊的數據結構,對數據存儲進行了優化,如壓縮表,對短數據進行壓縮存儲,再如,跳表,使用有序的數據結構加快讀取的速度。
-
還有一點,Redis采用自己實現的事件分離器,效率比較高,內部采用非阻塞的執行方式,吞吐能力比較大。
福利
大家還可以看看《京東Java十年老鳥,總結這些年工作、跳槽、面試之經歷》
https://blog.csdn.net/XingXing_Java/article/details/93008104
I/O多路復用技術(multiplexing)是什么?
鏈接:https://www.zhihu.com/question/28594409/answer/52835876
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
下面舉一個例子,模擬一個tcp服務器處理30個客戶socket。
假設你是一個老師,讓30個學生解答一道題目,然后檢查學生做的是否正確,你有下面幾個選擇:
這種模式就好比,你用循環挨個處理socket,根本不具有並發能力。
2. 第二種選擇:你 創建30個分身,每個分身檢查一個學生的答案是否正確。 這種類似於為每一個用戶創建一個進程或者線程處理連接。
3. 第三種選擇,你 站在講台上等,誰解答完誰舉手。這時C、D舉手,表示他們解答問題完畢,你下去依次檢查C、D的答案,然后繼續回到講台上等。此時E、A又舉手,然后去處理E和A。。。
這種就是IO復用模型,Linux下的select、poll和epoll就是干這個的。將用戶socket對應的fd注冊進epoll,然后epoll幫你監聽哪些socket上有消息到達,這樣就避免了大量的無用操作。此時的socket應該采用 非阻塞模式。
這樣,整個過程只在調用select、poll、epoll這些調用的時候才會阻塞,收發客戶消息是不會阻塞的,整個進程或者線程就被充分利用起來,這就是 事件驅動,所謂的reactor模式。
Redis I/O 多路復用
引出IO多路復用
為什么 Redis 中要使用 I/O 多路復用這種技術呢?
首先,Redis 是跑在單線程中的,所有的操作都是按照順序線性執行的,但是由於讀寫操作等待用戶輸入或輸出都是阻塞的,所以 I/O 操作在一般情況下往往不能直接返回,這會導致某一文件的 I/O 阻塞導致整個進程無法對其它客戶提供服務,而 I/O 多路復用就是為了解決這個問題而出現的。
要弄清問題先要知道問題的出現原因
由於進程的執行過程是線性的(也就是順序執行),當我們調用低速系統I/O(read,write,accept等等),進程可能阻塞,此時進程就阻塞在這個調用上,不能執行其他操作。阻塞很正常, 接下來考慮這么一個問題:一個服務器進程和一個客戶端進程通信,服
務器端read(sockfd1,bud,bufsize),此時客戶端進程沒有發送數據,那么read(阻塞調用)將阻塞直到客戶端write(sockfd,but,size)發來數據。在一個客戶和服務器通信時這沒什么問題,當多個客戶與服務器通信時,若服務器阻塞於其中一個客戶sockfd1,當另一個客戶的數據到達套接字sockfd2時,服務器仍不能處理,仍然阻塞在read(sockfd1,...)上。此時問題就出現了,不能及時處理另一個客戶的服務,腫么辦?I/O多路復用來解決!
繼續上面的問題,有多個客戶連接,sockfd1、sockfd2、sockfd3..sockfdn同時監聽這n個客戶,當其中有一個發來消息時就從select的阻塞中返回,然后就調用read讀取收到消息的sockfd,然后又循環回select阻塞;這樣就不會因為阻塞在其中一個上而不能處理另一個客戶的消息。
Q:
那這樣子,在讀取socket1的數據時,如果其它socket有數據來,那么也要等到socket1讀取完了才能繼續讀取其它socket的數據吧。那不是也阻塞住了嗎?而且讀取到的數據也要開啟線程處理吧,那這和多線程I/O有什么區別呢?
A:
1.CPU本來就是線性的,不論什么都需要順序處理,並行只能是多核CPU。
2.I/O多路復用本來就是用來解決對多個I/O監聽時,一個I/O阻塞影響其他I/O的問題,跟多線程沒關系。
3.跟多線程相比較,線程切換需要切換到內核進行線程切換,需要消耗時間和資源。而I/O多路復用不需要切換線/進程,效率相對較高,特別是對高並發的應用nginx就是用I/O多路復用,故而性能極佳。但多線程編程邏輯和處理上比I/O多路復用簡單,而I/O多路復用處理起來較為復雜。
理解IO多路復用
什么是I/O 多路復用
I/O 多路復用其實是在單個線程中通過記錄跟蹤每一個sock(I/O流) 的狀態來管理多個I/O流。結合下圖可以清晰地理解I/O多路復用。
select, poll, epoll 都是I/O多路復用的具體的實現。epoll性能比其他幾者要好。redis中的I/O多路復用的所有功能通過包裝常見的select、epoll、evport和kqueue這些I/O多路復用函數庫來實現的。
多路分離函數select
IO多路復用模型是建立在內核提供的多路分離函數select基礎之上的,使用select函數可以避免同步非阻塞IO模型中輪詢等待的問題。
如上圖所示,用戶線程發起請求的時候,首先會將socket添加到select中,這時阻塞等待select函數返回。當數據到達時,select被激活,select函數返回,此時用戶線程才正式發起read請求,讀取數據並繼續執行。
從流程上來看,使用select函數進行I/O請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操作,效率更差。但是,使用select以后最大的優勢是用戶可以在一個線程內同時處理多個socket的I/O請求。用戶可以注冊多個socket,然后不斷地調用select讀取被激活的socket,即可達到在同一個線程內同時處理多個I/O請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。
Reactor(反應器模式)
如上圖,I/O多路復用模型使用了Reactor設計模式實現了這一機制。通過Reactor的方式,可以將用戶線程輪詢I/O操作狀態的工作統一交給handle_events事件循環進行處理。用戶線程注冊事件處理器之后可以繼續執行做其他的工作(異步),而Reactor線程負責調用內核的select函數檢查socket狀態。當有socket被激活時,則通知相應的用戶線程(或執行用戶線程的回調函數),執行handle_event進行數據讀取、處理的工作。由於select函數是阻塞的,因此多路I/O復用模型也被稱為異步阻塞I/O模型。注意,這里的所說的阻塞是指select函數執行時線程被阻塞,而不是指socket。一般在使用I/O多路復用模型時,socket都是設置為NONBLOCK的,不過這並不會產生影響,因為用戶發起I/O請求時,數據已經到達了,用戶線程一定不會被阻塞。
總結
I/O 多路復用模型是利用select、poll、epoll可以同時監察多個流的 I/O 事件的能力,在空閑的時候,會把當前線程阻塞掉,當有一個或多個流有I/O事件時,就從阻塞態中喚醒,於是程序就會輪詢一遍所有的流(epoll是只輪詢那些真正發出了事件的流),依次順序的處理就緒的流,這種做法就避免了大量的無用操作。這里“多路”指的是多個網絡連接,“復用”指的是復用同一個線程。采用多路 I/O 復用技術可以讓單個線程高效的處理多個連接請求(盡量減少網絡IO的時間消耗),且Redis在內存中操作數據的速度非常快(內存內的操作不會成為這里的性能瓶頸),主要以上兩點造就了Redis具有很高的吞吐量。
參考:
https://baijiahao.baidu.com/s?id=1624003934114185747&wfr=spider&for=pc
https://www.cnblogs.com/syyong/p/6231326.html
https://blog.csdn.net/happy_wu/article/details/80052617
https://blog.csdn.net/tanswer_/article/details/70196139
一文讀懂I/O多路復用技術
前言
當我們要編寫一個echo服務器程序的時候,需要對用戶從標准輸入鍵入的交互命令做出響應。在這種情況下,服務器必須響應兩個相互獨立的I/O事件:1)網絡客戶端發起網絡連接請求,2)用戶在鍵盤上鍵入命令行。我們先等待哪個事件呢?沒有哪個選擇是理想的。如果在acceptor中等待一個連接請求,我們就不能響應輸入的命令。類似地,如果在read中等待一個輸入命令,我們就不能響應任何連接請求。針對這種困境的一個解決辦法就是I/O多路復用技術。基本思路就是使用select函數,要求內核掛起進程,只有在一個或多個I/O事件發生后,才將控制返回給應用程序。--《UNIX網絡編程》
我們以書中的這段描述來引出我們要講述的I/O多路復用技術。
I/O多路復用概述
I/O多路復用,I/O就是指的我們網絡I/O,多路指多個TCP連接(或多個Channel),復用指復用一個或少量線程。串起來理解就是很多個網絡I/O復用一個或少量的線程來處理這些連接。現在大部分講述I/O多路復用的文章用到的上面這張圖是《UNIX網絡編程》一書的。那么這也是當前我們理解I/O多路復用技術的基礎知識。從這張圖里面我們GET到哪些點呢?
個人理解有:
1、怎么區分的應用進程與內核
2、有兩次系統調用分別是select和recvfrom
3、兩次系統調用進程都阻塞
4、等待哪些數據准備好
下面我們逐一闡述。
二、用戶進程和內核
根據網絡OSI七層模型和網際網協議族的同比,我們可以知道這里說的用戶進程和內核是以傳輸層為分割線,傳輸層以上(不包括)是指用戶進程,傳輸層以下(包括)是指內核。上三層,web客戶端比如瀏覽器、web服務器這些都屬於應用層,里面跑的程序則是應用進程。下四層處理所有的通信細節,發送數據,等待確認,給無序到達的數據排序等等。這四層也是通常作為操作系統內核的一部分提供。由此可見圖1中說的系統調用的地方正是第四層和第五層之間的位置。
為了理解用戶進程和內核,再來看一張圖,網絡數據流向圖。也清晰的標明了用戶進程和內核的位置。值得注意的一點是客戶與服務器之間的信息流在其中一端是向下通過協議棧的,跨越網絡后,在另一端是向上通過協議棧的。這張圖描述的是局域網內,如果是在廣域網那么就是通過很多個路由器承載實際數據流。
三、select和recvfrom
3.1、select
理解了select就抓住了I/O多路復用的精髓,對應的操作系統中調用的則是系統的select函數,該函數會等待多個I/O事件(比如讀就緒,寫)的任何一個發生,並且只要有一個網絡事件發生,select線程就會執行。如果沒有任何一個事件發生則阻塞。我們在下面小節中會重點講述。函數如下:
-
-
-
int select(int maxfdpl,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
從這個函數的定義中的參數,我們能夠看出它描述的是,當調用select的時候告知內核對那些事件(讀就緒,寫)感興趣以及等待多長時間。
為了方便我們理解select調用,可以參照下面這張圖,是jdk的基於I/O多路復用技術的NIO實現。重點在於理解Selector復用器。
大致代碼如下:
ServerSocketChannel serverChannel = ServerSocketChannel.open();// 打開一個未綁定的serversocketchannel Selector selector = Selector.open();// 創建一個Selector serverChannel .configureBlocking(false);//設置非阻塞模式 serverChannel .register(selector, SelectionKey.OP_READ);//將ServerSocketChannel注冊到Selector while(true) { int readyChannels = selector.select(); if(readyChannels == 0) continue; Set selectedKeys = selector.selectedKeys(); Iterator keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) {//連接就緒 // a connection was established with a remote server. } else if (key.isReadable()) {//讀就緒 // a channel is ready for reading } else if (key.isWritable()) {//寫就緒 // a channel is ready for writing } keyIterator.remove(); } }
3.2、recvfrom
recvfrom一般用於UDP協議中,但是如果在TCP中connect函數調用后也可以用。用於從(已連接)套接口上接收數據,並捕獲數據發送源的地址。也就是我們本文中以及書中說的真正的I/O操作。
四、阻塞、非阻塞
這張圖可以看出阻塞式I/O、非阻塞式I/O、I/O復用、信號驅動式I/O他們的第二階段都相同,也就是都會阻塞到recvfrom調用上面就是圖中“發起”的動作。異步式I/O兩個階段都要處理。這里我們重點對比阻塞式I/O(也就是我們常說的傳統的BIO)和I/O復用之間的區別。
阻塞式I/O和I/O復用,兩個階段都阻塞,那區別在哪里呢?就在於第三節講述的Selector,雖然第一階段都是阻塞,但是阻塞式I/O如果要接收更多的連接,就必須創建更多的線程。I/O復用模式下在第一個階段大量的連接統統都可以過來直接注冊到Selector復用器上面,同時只要單個或者少量的線程來循環處理這些連接事件就可以了,一旦達到“就緒”的條件,就可以立即執行真正的I/O操作。這就是I/O復用與傳統的阻塞式I/O最大的不同。也正是I/O復用的精髓所在。
從應用進程的角度去理解始終是阻塞的,等待數據和將數據復制到用戶進程這兩個階段都是阻塞的。這一點我們從應用程序是可以清楚的得知,比如我們調用一個以I/O復用為基礎的NIO應用服務。調用端是一直阻塞等待返回結果的。
從內核的角度等待Selector上面的網絡事件就緒,是阻塞的,如果沒有任何一個網絡事件就緒則一直等待直到有一個或者多個網絡事件就緒。但是從內核的角度考慮,有一點是不阻塞的,就是復制數據,因為內核不用等待,當有就緒條件滿足的時候,它直接復制,其余時間在處理別的就緒的條件。這也是大家一直說的非阻塞I/O。實際上是就是指的這個地方的非阻塞。
當我們閱讀《UNIX網絡編程》(第三版)一書的時候。P124,6.2.3小節中“而不是阻塞在真正的I/O系統調用上”這里的阻塞是相對內核來說的。P127,6.2.7小節“因為其中真正的I/O操作(recvfrom)將阻塞進程”這里的阻塞是相對用戶進程來說的。明白了這兩點,理解起來就不矛盾了,而且一通到底!
五、適用場景
當服務程序需要承載大量TCP鏈接的時候,比如我們的消息推送系統,IM通訊,web聊天等等,在我們已經理解Selector原理的情況下,知道使用I/O復用可以用少量的線程處理大量的鏈接。I/O多路復用技術以事件驅動編程為基礎。它運行在單一進程上下文中,因此每個邏輯流都能訪問該進程的全部地址空間,這樣在流之間共享數據變得很容易。
六、總結
我們通常說的NIO大多數場景下都是基於I/O復用技術的NIO,比如jdk中的NIO,當然Tomcat8以后的NIO也是指的基於I/O復用的NIO。注意,使用NIO != 高性能,當連接數<1000,並發程度不高或者局域網環境下NIO並沒有顯著的性能優勢。如果放到線上環境,網絡情況在有時候並不穩定的情況下,這種基於I/O復用技術的NIO的優勢就是傳統BIO不可同比的了。那么使用select的優勢在於我們可以等到網絡事件就緒,那么用少量的線程去輪詢Selector上面注冊的事件,不就緒的不處理,就緒的拿出來立即執行真正的I/O操作。這個使得我們就可以用極少量的線程去HOLD住大量的連接。
轉載請注明作者及出處,並附上鏈接https://blog.csdn.net/wangxindong11/article/details/78591308
參考資料:
《UNIX網絡編程》、《深入理解計算機操作系統》