Netty原始碼分析--Channel註冊(中)(六)

Diligent_Watermelon發表於2019-07-02

        接上一篇,我們繼續看

    

    不知道大家第一次看這段程式碼的時候有沒有一臉懵逼,反正我是一臉懵,為什麼這個if else 最終都是呼叫的register0方法,都是一樣的。

    其實這裡就是為什麼Netty是執行緒安全的根本原因。

    我們先看下 eventLoop.inEventLoop() 方法

    

   

      第一張圖傳入了 當前的 執行緒, 第二個圖 判斷了 當前這個NioEventLoop中的Thread 是不是和當前執行緒相等, 如果相等返回true, 相反就是false.

      我們debug 看一下

    發現NioEventLoop中的Thread 當前並沒有賦值, 值是null,所以返回false.

   那麼程式碼也就進入到了

   

   這裡其實也容易漏看,其實這裡不只是啟動一個子執行緒來執行register0, 其實在這之前還做了好多時間。

   我們進入eventLoop的execute()方法,驚喜不。。。

    

    inEventLoop的值肯定是false,  然後執行addTask(task),把當前這個任務(register0)加入到佇列中,看下這個佇列

   

   

   這個佇列是一個LinkedBlockingQueue. 

   繼續

   

   

    這就是舉世聞名的CAS無鎖技術,當然不瞭解CAS的自行百度。這裡我想說的是,大家可以學習一下Netty這種寫法。

    

 

          

       CAS方式原子性更新state欄位的值,這裡的state一定要使用volatile修飾,這個關鍵字不太瞭解的,也自行百度。

       回到  startThread() 方法, 先檢查一下Thread 是否已經啟動, 如果沒有啟動,就把state原子性改成 啟動狀態 ,如果在啟動過程中出現異常,則再次把state原子性改成 未啟動狀態。

       繼續進入 doStartThread() 方法

       

       先是一個斷言來保證thread一定是null, 然後啟動一個子執行緒,並把當前這個子執行緒 賦值給了當前的 這個NioEventLoop 中的 thread 成員變數。 ok ,到現在為止,NioEventLoop 中的唯一執行緒確定。

      

      從這裡我們進入run() 方法

      

      我們發現進入到了一個死迴圈, 然後裡面有一個switch分支,我們來看下里面的策略計算方法。

      在說這個之前我們再來一起看一個NIO中多路複用器的API

     

       不會阻塞,不管什麼通道就緒都立刻返回(譯者注:此方法執行非阻塞的選擇操作。如果自從前一次選擇操作後,沒有通道變成可選擇的,則此方法直接返回零。

       同時這個方法會清除wakeup()方法的效果。

    

       此方法執行阻塞的 selection operation 。 只有在選擇了至少一個通道之後,才會返回此選擇器的wakeup方法,當前執行緒被中斷,或給定的超時期限到期,以先到者為準。

       此方法不提供實時保證:它呼叫超時,就像呼叫Object.wait(long)方法一樣。

    

      如果另一個執行緒在呼叫select()select(long)方法時被阻止,則該呼叫將立即返回。 如果當前沒有選擇操作,則下一次呼叫這些方法之一將立即返回,除非在此期間呼叫selectNow()方法。 無論如何,該呼叫返回的值可能不為零。 的後續呼叫select()select(long)除非此方法在此期間再次呼叫的方法將阻塞如常。 在兩次連續的選擇操作之間多次呼叫此方法與呼叫它一樣具有相同的效果

     

    好了,瞭解了這些我們繼續看,

    

    先檢查是否有待處理的task,如果有那麼就非阻塞的檢查一下是否有新的channel被註冊,然後返回channel註冊的數量,可能是0, 如果沒有task,則返回 - 1

    我們發現如果有task,那麼這麼switch就直接跳出了。如果返回 - 1 ,就執行 select(wakenUp.getAndSet(false))

    我們先看下沒有task的情況吧。先大概讀一下這一大段註釋

   

   大概的意思是說:

   在呼叫選擇器喚醒方法,之前,先確定wakenUp的值,以減少喚醒負載,因為喚醒選擇器是一個耗時的操作。  但是不能把warkup設定true太早,將會觸發競爭。

   1、選擇器在wakenUp屬性更新為false和選擇操作之間被喚醒

   2、選擇器在選擇操作和獲取wakenUp屬性之間

   在第一種情況下,當wakenUp屬性更新為true,接下來的選擇操作就會立刻被喚醒, 直到在下一次迴圈中wakenUp屬性更新為false,wakenUp.compareAndSet(false, true)  ,將會失敗,同時引起下一次不必要的選擇操作阻塞, 怎麼這句話呢(自己的理解)。

   我們看一下這個方法 select(wakenUp.getAndSet(false))

   首先我們假如入參當前是false, 也就是 oldWakenUp = false

   

    那麼再假如當前是有task待處理的,那麼也就是說  hasTasks() && wakenUp.compareAndSet(false, true)  == true , 那麼將執行selectNow(), 也就是當前時間到上一次select操作的期間內是否有channel註冊進來。

    然後break,接下來

   wakeUp 剛剛被CAS 成 true ,所以這裡會執行wakeup操作,也就意味著下一次select操作將會被立即返回。

   接下來就是去處理task 和 新接入的客戶端或者讀寫操作了(一會再說這個)。 

   因為是死迴圈,我們繼續回來,又到了 

   

    這次的wakeUp 變成了true, 並且把狀態置為false, 那麼也就是說  oldWakenUp = true 

    

      這裡不管有沒有任務,都會立即返回,因為我們之前執行了selector.wakeup(),這裡我自己猜測可能是因為處理讀寫和任務用掉了很長時間,所以這裡直接就檢查當前是會有channel已經註冊進來已經在等待了。

      如果有的話,直接break.去執行。 

      當然如果之前沒有 selector.wakeup() 過,那麼將會執行 1s 的時間,看著1s 內是否有新的channel進來。

      

        

       繼續看,通過這兩段我們發現如果迴圈超時了,那麼將會break掉。

      

 

    

      通過這兩段我們發現,當迴圈512次之後,那麼將會重建Selector

       

      這裡其實是因為JDK的BUG導致的,會把CPU飈到100%

      整個重建的過程其實就是,建立新的selector,把老的上面的 SelectionKey 都註冊到新的selector上,然後將老的selector關閉掉,具體的內容就不一起看了。

  

相關文章