c++ 多个线程操作socket要同步吗_基础知识深化:NIO优化原理和Tomcat线程模型
1、I/O阻塞
書上說BIO、NIO等都屬于I/O模型,但是I/O模型這個范圍有點含糊,我為此走了不少彎路。我們日常開發過程中涉及到NIO模型應用,如Tomcat、Netty中等線程模型,可以直接將其視為 網絡I/O模型 。本文還是在基礎篇章中介紹幾種I/O模型方式,后面就默認只講解網絡I/O模型了。
1.1、I/O分類
BIO、NIO、AIO等都屬于I/O模型,所以它們優化的都是系統I/O的性能,因此首先,我們要清楚常見的I/O有哪些分類:
1.2、I/O過程和性能
I/O(Input/Output)即數據的輸入/輸出,為什么大家很關心I/O的性能呢?因為I/O存在的范圍很廣,在高并發的場景下,這部分性能會被無限放大。而且與業務無關,是可以有統一解決方案的。
所有的系統I/O都分為兩個階段:等待就緒和數據操作。舉例來說,讀函數,分為等待系統可讀和真正的讀;同理,寫函數分為等待網卡可以寫和真正的寫:
需要說明的是等待就緒的阻塞是不使用CPU的,是在“空等”;而真正的讀寫操作的阻塞是使用CPU的,真正在”干活”,而且這個過程非常快,屬于memory copy,帶寬通常在1GB/s級別以上,可以理解為基本不耗時。這就出現一個奇怪的現象 -- 不使用CPU的“等待就緒”,卻比實際使用CPU的“數據操作”,占用CPU時間更多 。
傳統阻塞I/O模型,即在讀寫數據過程中會發生阻塞現象。當用戶線程發出I/O請求之后,內核會去查看數據是否就緒,如果沒有就緒就會等待數據就緒,而用戶線程就會處于阻塞狀態,用戶線程交出CPU。當數據就緒之后,內核會將數據拷貝到用戶線程,并返回結果給用戶線程,用戶線程才會解除block狀態。
明確的是,讓當前工作線程阻塞,等待數據就緒,是很浪費線程資源的事情,上述三種I/O都有一定的優化方案:
- 磁盤I/O :現代電腦中都有一個DMA(Direct Memory Access 直接內存訪問) 的外設組件,可以將I/O數據直接傳送到主存儲器中并且傳輸不需要CPU的參與,以此將CPU解放出來去完成其他的事情。
- 網絡I/O :NIO、AIO等I/O模型,通過向事件選擇器注冊I/O事件,基于就緒的事情來驅動執行I/O操作,避免的等待過程。
- 內存I/O :內存部分沒涉及到太多阻塞,優化點在于減少用戶態和內核態之間的數據拷貝。nio中的零拷貝就有mmap和sendfile等實現方案。
1.3、網絡I/O阻塞
這里仔細的講講網絡I/O模型中的阻塞,即socket的阻塞。在計算機通信領域,socket 被翻譯為“套接字”,它是計算機之間進行通信的一種約定或一種方式,是在tcp/ip協議上,抽象出來的一層網絡通訊協議。
同上面I/O的過程一樣,網絡I/O也同樣分成兩個部分:
每個 socket 被創建后,都會分配兩個緩沖區,輸入緩沖區和輸出緩沖區:
- 輸入緩沖區 :當使用 read()/recv() 讀取數據時,(1)首先會檢查緩沖區,如果緩沖區中有數據,那么就讀取,否則函數會被阻塞,直到網絡上有數據到來。(2)如果要讀取的數據長度小于緩沖區中的數據長度,那么就不能一次性將緩沖區中的所有數據讀出,剩余數據將不斷積壓,直到有 read()/recv() 函數再次讀取。(3)直到讀取到數據后 read()/recv() 函數才會返回,否則就一直被阻塞。
- 輸出緩沖區 :當使用 write()/send() 發送數據時,(1)首先會檢查緩沖區,如果緩沖區的可用空間長度小于要發送的數據,那么 write()/send() 會被阻塞(暫停執行),直到緩沖區中的數據被發送到目標機器,騰出足夠的空間,才喚醒 write()/send() 函數繼續寫入數據。(2) 如果TCP協議正在向網絡發送數據,那么輸出緩沖區會被鎖定,不允許寫入,write()/send() 也會被阻塞,直到數據發送完畢緩沖區解鎖,write()/send() 才會被喚醒。(3)如果要寫入的數據大于緩沖區的最大長度,那么將分批寫入。(4)直到所有數據被寫入緩沖區 write()/send() 才能返回。
由此可見在網絡I/O中,會有很多的因素導致數據的讀取和寫入過程出現阻塞,創建socket連接也一樣。socket.accept()、socket.read()、socket.write()這類函數都是同步阻塞的,當一個連接在處理I/O的時候,系統是阻塞的,該線程當前的cpu時間片就浪費了。
2、阻塞優化
2.1、BIO、NIO、AIO
BIO、NIO、AIO對比
以socket.read()為例子:
- 傳統的BIO里面socket.read(),如果TCP RecvBuffer里沒有數據,函數會一直阻塞,直到收到數據,返回讀到的數據。
- 對于NIO,如果TCP RecvBuffer有數據,就把數據從網卡讀到內存,并且返回給用戶;反之則直接返回0,永遠不會阻塞。
- 最新的AIO(Async I/O)里面會更進一步:不但等待就緒是非阻塞的,就連數據從網卡到內存的過程也是異步的。
換句話說,BIO里用戶最關心“我要讀”,NIO里用戶最關心”我可以讀了”,在AIO模型里用戶更需要關注的是“讀完了”。
NIO
NIO的優化體現在兩個方面:
NIO一個重要的特點是: socket主要的讀、寫、注冊和接收函數,在等待就緒階段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高) 。
NIO的主要事件有幾個:讀就緒、寫就緒、有新連接到來。
我們首先需要注冊當這幾個事件到來的時候所對應的處理器。然后在合適的時機告訴事件選擇器:我對這個事件感興趣。對于寫操作,就是寫不出去的時候對寫事件感興趣;對于讀操作,就是完成連接和系統沒有辦法承載新讀入的數據的時;對于accept,一般是服務器剛啟動的時候;而對于connect,一般是connect失敗需要重連或者直接異步調用connect的時候。
其次,用一個死循環選擇就緒的事件,會執行系統調用 (Linux 2.6之前是select、poll,2.6之后是epoll,Windows是IOCP) ,還會阻塞的等待新事件的到來。新事件到來的時候,會在selector上注冊標記位,標示可讀、可寫或者有連接到來。
2.2、Reactor模式
Reactor模式稱之為響應器模式,通常用于 NIO 非阻塞IO的網絡通信框架中。Reactor設計模式用于處理由一個或多個客戶端并發傳遞給應用程序的的服務請求,可以理解成, Reactor模式是用來實現網絡NIO的方式 。
Reactor是一種事件驅動機制,是處理并發I/O常見的一種模式,用于同步I/O,其中心思想是將所有要處理的I/O事件注冊到一個中心I/O多路復用器上,同時主線程阻塞在多路復用器上,一旦有I/O事件到來或是準備就緒,多路復用器將返回并將相應I/O事件分發到對應的處理器中。
Reactor模式主要分為下面三個部分:
2.3、三種Reactor模式
單線程Reactor模式
一個線程:
- 單線程:建立連接(Acceptor)、監聽accept、read、write事件(Reactor)、處理事件(Handler)都只用一個單線程。
多線程Reactor模式
一個線程 + 一個線程池:
- 單線程:建立連接(Acceptor)和 監聽accept、read、write事件(Reactor),復用一個線程。
- 工作線程池:處理事件(Handler),由一個工作線程池來執行業務邏輯,包括數據就緒后,用戶態的數據讀寫。
主從Reactor模式
三個線程池:
- 主線程池:建立連接(Acceptor),并且將accept事件注冊到從線程池。
- 從線程池:監聽accept、read、write事件(Reactor),包括等待數據就緒時,內核態的數據I讀寫。
- 工作線程池:處理事件(Handler),由一個工作線程池來執行業務邏輯,包括數據就緒后,用戶態的數據讀寫。
3、Tomcat線程模型
3.1、Api網絡請求過程
我們先補一下基礎知識,講解后端接口的響應過程。一個http連接里,完整的網絡處理過程一般分為accept、read、decode、process、encode、send這幾步:
3.2、各個線程模型
在tomcat的各個版本中,所支持的線程模型也發生了一步步演變。一方面,直接將默認線程模型,從BIO變成了NIO。另一方面,在后續幾個版本中,加入了對AIO和APR線程模型的支持,這里要注意,僅僅是支持,而非默認線程模型。
- BIO :阻塞式IO,tomcat7之前默認,采用傳統的java IO進行操作,該模式下每個請求都會創建一個線程,適用于并發量小的場景。
- NIO :同步非阻塞,比傳統BIO能更好的支持大并發,tomcat 8.0 后默認采用該模式。
- AIO :異步非阻塞 (NIO2),tomcat8.0后支持。多用于連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用OS參與并發操作,編程比較復雜。
- APR :tomcat 以JNI形式調用http服務器的核心動態鏈接庫來處理文件讀取或網絡傳輸操作,需要編譯安裝APR庫(也就是說IO操作的部分直接調用native代碼實現)。
各個線程模型中,NIO是作為目前最實用的線程模型,因此也是目前Tomcat默認的線程模型,因此本文對此著重講解。
3.3、BIO和NIO
BIO模型
在BIO模型中,主要參與的角色有: Acceptor 和 Handler工作線程池 。對應于前文中Api的請求過程,它們的分工如下:
- Acceptor :Accepter線程專門負責建立網絡連接( accept )。新連接創建后,交給Handler工作線程池處理請求。
- Handlers :針對每個請求的連接,Handler工作線程池都會分配一個線程,執行后面的所有步驟( read、decode、process、encode、send )。
前文的知識點有鋪墊, read 和 send 是面向網絡I/O的,在等待讀寫就緒過程中,其實是CPU阻塞的。因此Handler工作線程池中的每個線程,都會因為I/O阻塞而“空等待”,造成浪費。
NIO模型
tomcat的NIO模型,相比較于BIO模型,多了個Poller角色: Acceptor 、 Poller 和 Handler工作線程池 。這三個角色是不是很熟悉,如果將Poller換成Reactor,是不是就是Reactor模型。沒錯,tomcat的nio模型,的確就是基于 主從Reactor模型 ,只不過將Reactor換了個名字而已。
- Acceptor :Accepter線程專門負責建立網絡連接( accept )。新連接創建后,不是直接使用Worker線程處理請求,而是先將請求發送給Poller緩沖隊列。
- Poller :在Poller中,維護了一個Selector對象,當Poller從緩沖隊列中取出連接后,注冊到該Selector中,阻塞等待讀寫就緒( read等待就緒、send等待就緒 )。
- Handlers :遍歷Selector,找出其中就緒的IO操作,并交給Worker線程處理( read內存讀、decode、process、encode、send內存寫 )。
對比
- BIO模型中,一個線程對應一個請求連接的完整過程,因此tomcat服務能處理的最大連接數,和最大線程數一致。
- NIO模型中,在一個請求連接中,對應的一個工作線程,只處理I/O讀寫就緒后的非阻塞過程。因此tomcat服務能處理的最大連接數,要遠大于最大線程數量。
3.4、參數設置
針對于tomcat的nio模型,可以做一些參數設置。因為springboot是內嵌tomcat的,這些參數設置同樣可以在properties配置文件中定義:
- 最大線程數(server.tomcat.threads.max) :工作線程池的最大線程數,默認200。注意不是越大越好,如果線程數過大,那么CPU會花費大量的時間用于線程的切換,整體效率會降低。
- 最小線程數(server.tomcat.threads.min-spare) :工作線程池的最小線程數,默認10。
- 最大等待數(server.tomcat.accept-count) :當調用HTTP請求數達到tomcat的最大線程數時,還有新的HTTP請求到來,這時tomcat會將該請求放在等待隊列中,這個acceptCount就是指能夠接受的最大等待數,默認100。如果等待隊列也被放滿了,這個時候再來新的請求就會被tomcat拒絕。
- 最大連接數(server.tomcat.max-connections) :在同一時間,tomcat能夠接受的最大連接數,默認8192。
4、常見問題
1、tomcat運行后,出現 nio-8080-exec- 前綴的線程作用是什么?
是工作線程池中的線程。你們可以觀察某個springboot運行項目的線程模型,由于基本都是基于nio模型的tomcat應用,因此都包括這些線程:
- 1個名稱中包含Accepter的線程。
- 2個名稱中包含Poller的線程。
- 10個工作線程,名稱從 nio-8080-exec-1 到 nio-8080-exec-10。如果并發交高,默認最多有200個線程,名稱到 nio-8080-exec-200。
2、tomcat中nio模型中,存在poller單線程讀取多個請求線程的數據,會不會出現線程安全問題?因為通過會使用ThreadLocal存儲請求用戶身份信息。
不會。因為poller只是處理等待讀就緒的環節,一旦讀就緒事件觸發后,真正的讀取數據和處理業務邏輯,都是由工作線程池中的某個線程跟到底,可以放心大膽使用ThreadLocal。
3、為什么我自己對比測試nio和bio,性能提升不大?
nio線程模型優化的是線程利用率,為了在高并發場景下,基于有限的線程資源,處理更多的請求連接。
例如:tomcat使用默認最大線程數200,但你的并發請求數量連200都不到,就算是BIO模型,線程池中200個線程都沒利用完。這時候你用NIO還是BIO,區別不大,甚至BIO模型處理還更快一些。但如果你的并發請求數到了2000、20000,BIO模型就會出現性能瓶頸了,超過200的請求都會阻塞住,而NIO模型就能大展身手。
來源:https://www.tuicool.com/articles/FbAbArq
總結
以上是生活随笔為你收集整理的c++ 多个线程操作socket要同步吗_基础知识深化:NIO优化原理和Tomcat线程模型的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: ASP.NET(c#)常用类函数
- 下一篇: 数据采集简介
