多線程調度瓶頸分析

一、多線程調度瓶頸分析

之前加強版的調度流程,在之前的調度流程當中,有一個專屬的Accepter的線程,用來做我們的連接。當有新的連接的時候,將它們添加到連接隊列。在連接隊列中可以分別進行輸入、輸出的selector的線程,這個線程命名爲Processor。當數據就緒的時候,會把scoket通道拿出來,再把任務解析出來,丟到線程池當中去。在線程池中處理對應的reader或者writer。

想法非常簡單,當有事件的時候,就把事件丟給線程池去完成,儘可能地發揮cpu的能力。但是在這個過程中遇到非常多的問題,在這之前有20多個線程,在這20多個線程中,真正工作的只有2個。一個是讀,一個是寫,在某個時刻,只有這兩個,在這個時刻過去之後,纔會有另外的線程又來運行。所以線程池的最大本質並沒有發揮它的作用,這主要是因爲相互在競爭鎖導致的性能低下。

 

二、死鎖問題

此時有X、Y兩個線程,有兩個不同的資源A、B,紅點代表我們的資源。

如果線程X優先去拿資源A,此時B也被Y線程拿到,如果資源是被獨佔的,那麼需要用鎖把它保護起來。此時分別會給A、B資源加上鎖,A資源的鎖被線程X所持有,B資源的鎖被線程Y所持有。此時線程X要想獲取資源B,Y線程要想獲取資源A,它將獲取不到。爲什麼呢?此時線程X想要獲取到的資源B已經被線程Y獲取到了,線程Y所要獲取到的資源A已經被線程X獲取到了,它們兩者相互等待。若是在這樣的情況下,就會出現兩個線程都被阻塞掉,都被等待了。兩個線程等待,而且是一個死等待,X永遠等待資源B釋放,而Y永遠等待資源A釋放。

若我們的邏輯是X線程想要釋放資源A必須拿到資源B做一定的操作之後纔會釋放資源A的話,這時就會出現問題,線程Y不釋放資源B,線程X永遠永遠得不到後續的運行。如果線程Y也是同樣的邏輯,那麼就會出現線程Y永遠等待線程X釋放,線程X永遠等待線程Y釋放,兩者相互等待,出現死鎖。這是死鎖的問題,但是我們當前的問題並不是死鎖的問題,而是當前的鎖被佔用之後,其它的線程一段時間之內無法得到運行的問題。

 

三、多線程調度瓶頸

有一個靜態資源,也是一個需要鎖去保護的資源,這個資源就是selector,這個selector有可能是selector的一個輸入,也可能是selector的一個輸出,不管怎樣,每個selector都有一個對應的selector thread。這個Selector Thread永遠在循環selector裏面的一個selector方法。

此時有一個線程池,線程池中有非常多得到子線程,這些子線程都想獲取到selector這個變量,因爲它要註冊或者反註冊。一旦要註冊或者反註冊,那麼就需要對selector進行喚醒,然後再進行後續的邏輯,涉及到的方法有registerChannel()、select()、wakeup(),而這些操作我們使用一個AtomicBoolean值來保護。由於這個值,導致了在某一個時刻,在Selector Thread獲取到資源的時候,其它的子線程無法進行註冊或解綁。如果子線程想要註冊或解綁,一定要讓當前的Selector Thread將資源釋放掉。如果想要釋放,就需要調用wakeup(),讓它釋放掉後子線程馬上拿到鎖,然後子線程再處理其它事情。子線程處理完畢後再通知Selector Thread, Selector Thread再去循環。而在循環的過程中,也有可能被其它的線程所持有。另外的線程也有可能會出現想要註冊、解綁,當連接量越多,這個機率就越大。而越多,導致了競爭越激烈,越激烈就導致了線程之間的切換。從SelectorThread切換到某一個子線程,或者是某一個子線程切換到SelectorThread都是一樣的。資源之間的切換,浪費的是cpu。cpu其實大部分時間消耗到了線程切換上面,而不是真正地幹事情,所以就導致了性能低下。

而之前進行了一下簡單的優化,將線程數量減小,然後直接進行輸出,而不是進行註冊後調度輸出,這樣就得到了線程性能上的調優。

 

四、發送數據調度優化

有一個靜態資源,就是selector,Loop Thread對其進行循環,之後有一個主線程,是我們自己自定義的要發送數據的子線程。

子線程如果要發送數據,需要先註冊一個輸出,而註冊輸出涉及到一個鎖的獲取及釋放的過程。這個過程詳細可以理解爲如下,首先會把selector喚醒,也就是讓當前的Loop Thread進行一個暫停,等待當前線程後續執行完之後再進行循環。子線程調用wakeup()之後拿到鎖,然後進行註冊,註冊完畢後通知註冊完成,然後Loop Thread繼續循環。當Loop Thread循環到子線程就緒可以進行輸出後,通知子線程可以進行處理,此時的操作就是handSelection,而handSelection內部就是selectionKey,SelectionKey拿出之後就可以拿到當前的通道,而拿到通道之後可以在通道中做一些事情,而這些事情不是當前的Loop Thread去做,而是會把這些操作丟到一個線程池中,讓線程池來做這些操作,即向線程池中丟一個任務。這個任務真正被執行的時候,纔是它被寫數據的時候。當寫完的時候,會判斷當前任務還有沒有,如果還有,會再次嘗試喚醒、註冊,再去處理輸出的流程,這是要閉環的。

這是之前的流程,這個流程錯在哪裏呢?

錯在想要進行數據的輸出是非常迫切的,就是想進行一個數據的輸出,但是中間執行了過多的步驟,喚醒->註冊->就緒->放入線程池->執行輸出任務,但是往往只有進行輸出的操作,這些數據都是可以輸出出去的。也就代表了當前這個socket,其實90%的可能都是已經就緒的,它就可以進行數據的輸出。若發送100字節的數據,可能發送了99個字節,還有1個字節發送不出去,有這樣的情況,此時再來進行註冊操作是可以的。但是若本身可以發送99個數據的情況下,就進行了前面這麼多的步驟再去發送數據,把最後1個字節還進行這樣的操作,那就相當於2遍調度消耗。

 

五、優化之後的模型

也是一個Selector Thread進行循環,子線程直接進行輸出,若輸出成功,則代表數據發送完成,發送下一條數據。如果沒有發送成功,則代表當前的socket可能沒有就緒,則進行後續的喚醒->註冊->循環->處理->丟到線程池->取出任務進行執行,此時做這個操作是可以的。輸出之後再執行write輸出操作,這就是簡化之後的操作。

這個操作非常明確,我優先做的事情就是輸出,我先嚐試一下能不能進行輸出,如果不能輸出數據,也耗費不了多長時間。如果能夠輸出數據,則免去了後續的一系列流程,所以就讓我們的調度得到了優化。

 

六、總結

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章