目錄大綱:
- 前言
- 處理耗時業務的第一種方式-------handler 種加入線程池
- 處理耗時業務的第二種方式-------Context 中添加線程池
- 總結:兩種方式的對比和思考
前言
熟悉 Netty 的同學都知道,不能在 Netty 中做耗時的,不可預料的操作,比如數據庫,網絡請求,這將會嚴重影響 Netty 對 Socket 的處理速度。而解決方法就是將耗時任務添加到異步線程池中。但就添加線程池這步操作來講,可以有2種方式,而且這2種方式的區別也蠻大的。今天就好好講一講。
1. 處理耗時業務的第一種方式-------handler 種加入線程池
以我們之前的 Netty 的 demo 源碼例子,在 channelRead 方法種進行異步:
上圖中的 channelRead 方法,我們模擬了一個耗時 10 秒的操作,於是,我們將這個任務提交到了一個自定義的業務線程池中,這樣,就不會阻塞 Netty 的 IO 線程。
這樣操作之后,整個程序的邏輯是這樣的:
解釋一下上圖,當 IO 線程輪詢到一個 socket 事件,然后,IO 線程開始處理,當走到耗時 handler 的時候,將耗時任務交給業務線程池。當耗時任務執行完畢再執行 pipeline write 方法的時候(代碼中使用的是 context 的 write 方法,上圖畫的是執行 pipeline 方法),會將任務這個任務交給 IO 線程。
下面是 write 方法的源碼:
當判定下個 outbound 的 executor 線程不是當前線程的時候,會將當前的工作封裝成 task ,然后放入 mpsc 隊列中,等待 IO 任務執行完畢后執行隊列中的任務。很明顯,下個任務的線程肯定是 IO 線程,因為我們沒有設置。
2. 處理耗時業務的第二種方式-------Context 中添加線程池
第二種方式是 Netty 建議的方式,在添加 pipeline 中的 handler 時候,添加一個線程池:
而 handler 中的代碼不用做任何修改:
當我們在調用 addLast 方法添加線程池后,handler 將優先使用這個線程池,如果不添加,將使用 IO 線程。
所以,當走到 AbstractChannelHandlerContext 的 invokeChannelRead 方法的時候,executor.inEventLoop() 是不會通過的,因為當前線程是 IO 線程,Context(也就是 Handler) 的 executor 是業務線程,所以會異步執行,如下:
這個時候,后面的整個流程就變成和第一個方式一樣了。
總結: 兩種方式的對比和思考
有什么區別呢?第一種方式在 handler 中添加異步,可能更加的自由,比如如果需要訪問數據庫,那我就異步,如果不需要,就不異步,異步會拖長接口響應時間。因為需要將任務放進 mpscTask 中。如果不湊巧,IO 時間很短,task 很多,可能一個循環下來,都沒時間執行整個 task,導致響應時間達不到指標。
第二種方式是 Netty 建議的,但是,這么做會將整個 handler 都交給業務線程池。不論耗時不耗時,都加入到隊列里,不夠靈活。
再回顧一下我們剛開始的圖吧:
個人建議還是使用第一種方式,比較靈活。