成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

淺析 Jetty 中的線程優化思路

開發
本文介紹了 Jetty 中 ManagedSelector 和 ExecutionStrategy 的設計實現,通過與原生 select 調用的對比揭示了 Jetty 的線程優化思路。Jetty 設計了一個自適應的線程執行策略(EatWhatYouKill),在不出現線程饑餓的情況下盡量用同一個線程偵測 I/O 事件和處理 I/O 事件,充分利用了 CPU 緩存并減少了線程切換的開銷。這種優化思路

一、什么是 Jetty

Jetty 跟 Tomcat 一樣是一種 Web 容器,它的總體架構設計如下:

Jetty 總體上由一系列 Connector、一系列 Handler 和一個 ThreadPool組成。

圖片

Connector 也就是 Jetty 的連接器組件,相比較 Tomcat 的連接器,Jetty 的連接器在設計上有自己的特點。

Jetty 的 Connector 支持 NIO 通信模型,NIO 模型中的主角是 Selector,Jetty 在 Java 原生 Selector 的基礎上封裝了自己的 Selector:ManagedSelector。

二、Jetty 中的 Selector 交互

2.1 傳統的 Selector 實現

常規的 NIO 編程思路是將 I/O 事件的偵測和請求的處理分別用不同的線程處理。

具體過程是:

  1. 啟動一個線程;
  2. 在一個死循環里不斷地調用 select 方法,檢測 Channel 的 I/O 狀態;
  3. 一旦 I/O 事件到達,就把該 I/O 事件以及一些數據包裝成一個 Runnable;
  4. 將 Runnable 放到新線程中去處理。

這個過程有兩個線程在干活:一個是 I/O 事件檢測線程、一個是 I/O 事件處理線程。

這兩個線程是"生產者"和"消費者"的關系。

這樣設計的好處:

將兩個工作用不同的線程處理,好處是它們互不干擾和阻塞對方。

這樣設計的缺陷:

當 Selector 檢測讀就緒事件時,數據已經被拷貝到內核中的緩存了,同時 CPU 的緩存中也有這些數據了。

這時當應用程序去讀這些數據時,如果用另一個線程去讀,很有可能這個讀線程使用另一個 CPU 核,而不是之前那個檢測數據就緒的 CPU 核。

這樣 CPU 緩存中的數據就用不上了,并且線程切換也需要開銷。

2.2 Jetty 中的 ManagedSelector 實現

Jetty 的 Connector 將 I/O 事件的生產和消費放到同一個線程處理。

如果執行過程中線程不阻塞,操作系統會用同一個 CPU 核來執行這兩個任務,這樣既能充分利用 CPU 緩存,又可以減少線程上下文切換的開銷。

ManagedSelector 本質上是一個 Selector,負責 I/O 事件的檢測和分發。

為了方便使用,Jetty 在 Java 原生 Selector 的基礎上做了一些擴展,它的成員變量如下:

public class ManagedSelector extends ContainerLifeCycle implements Dumpable
{
    // 原子變量,表明當前的ManagedSelector是否已經啟動
    private final AtomicBoolean _started = new AtomicBoolean(false);
     
    // 表明是否阻塞在select調用上
    private boolean _selecting = false;
     
    // 管理器的引用,SelectorManager管理若干ManagedSelector的生命周期
    private final SelectorManager _selectorManager;
     
    // ManagedSelector的id
    private final int _id;
     
    // 關鍵的執行策略,生產者和消費者是否在同一個線程處理由它決定
    private final ExecutionStrategy _strategy;
     
    // Java原生的Selector
    private Selector _selector;
     
    // "Selector更新任務"隊列
    private Deque<SelectorUpdate> _updates = new ArrayDeque<>();
    private Deque<SelectorUpdate> _updateable = new ArrayDeque<>();
     
    ...
}

2.2.1 SelectorUpdate 接口

為什么需要一個"Selector更新任務"隊列呢?

對于 Selector 的用戶來說,我們對 Selector 的操作無非是將 Channel 注冊到 Selector 或者告訴 Selector 我對什么 I/O 事件感興趣。

這些操作其實就是對 Selector 狀態的更新,Jetty 把這些操作抽象成 SelectorUpdate 接口。

/**
 * A selector update to be done when the selector has been woken.
 */
public interface SelectorUpdate
{
    void update(Selector selector);
}

這意味著不能直接操作 ManagedSelector 中的 Selector,而是需要向 ManagedSelector 提交一個任務類。

這個類需要實現 SelectorUpdate 接口的 update 方法,在 update 方法中定義要對 

ManagedSelector 做的操作。

比如 Connector 中的 Endpoint 組件對讀就緒事件感興趣。

它就向 ManagedSelector 提交了一個內部任務類

ManagedSelector.SelectorUpdate:

_selector.submit(_updateKeyAction);

這個 _updateKeyAction 就是一個

SelectorUpdate 實例,它的 update 方法實現如下:

private final ManagedSelector.SelectorUpdate _updateKeyAction = new ManagedSelector.SelectorUpdate()
{
    @Override
    public void update(Selector selector)
{
        // 這里的updateKey其實就是調用了SelectionKey.interestOps(OP_READ);
        updateKey();
    }
};

在 update 方法里,調用了 SelectionKey 類的 interestOps 方法,傳入的參數是 OP_READ,意思是我對這個 Channel 上的讀就緒事件感興趣。

2.2.2 Selectable 接口

上面有了 update 方法,那誰來執行這些 update 呢,答案是 ManagedSelector 自己。

它在一個死循環里拉取這些 SelectorUpdate 任務逐個執行。

I/O 事件到達時,ManagedSelector 通過一個任務類接口(Selectable 接口)來確定由哪個函數處理這個事件。

public interface Selectable
{
    // 當某一個Channel的I/O事件就緒后,ManagedSelector會調用的回調函數
    Runnable onSelected();
 
    // 當所有事件處理完了之后ManagedSelector會調的回調函數
    void updateKey();
}

Selectable 接口的 onSelected() 方法返回一個 Runnable,這個 Runnable 就是 I/O 事件就緒時相應的處理邏輯。

ManagedSelector 在檢測到某個 Channel 上的 I/O 事件就緒時,ManagedSelector 調用這個 Channel 所綁定的類的 onSelected 方法來拿到一個 Runnable。

然后把 Runnable 扔給線程池去執行。

三、Jetty 的線程優化思路

3.1 Jetty 中的 ExecutionStrategy 實現

前面介紹了 ManagedSelector 的使用交互:

  1. 如何注冊 Channel 以及 I/O 事件
  2. 提供什么樣的處理類來處理 I/O 事件

那么 ManagedSelector 如何統一管理和維護用戶注冊的 Channel 集合呢,答案是

ExecutionStrategy 接口。

這個接口將具體任務的生產委托給內部接口 Producer,而在自己的 produce 方法里實現具體執行邏輯。

這個 Runnable 的任務可以由當前線程執行,也可以放到新線程中執行。

public interface ExecutionStrategy
{
    // 只在HTTP2中用到的一個方法,暫時忽略
    public void dispatch();
 
    // 實現具體執行策略,任務生產出來后可能由當前線程執行,也可能由新線程來執行
    public void produce();
     
    // 任務的生產委托給Producer內部接口
    public interface Producer
    {
        // 生產一個Runnable(任務)
        Runnable produce();
    }
}

實現 Produce 接口生產任務,一旦任務生產出來,ExecutionStrategy 會負責執行這個任務。

private class SelectorProducer implements ExecutionStrategy.Producer
{
    private Set<SelectionKey> _keys = Collections.emptySet();
    private Iterator<SelectionKey> _cursor = Collections.emptyIterator();
 
    @Override
    public Runnable produce()
{
        while (true)
        {
            // 如果Channel集合中有I/O事件就緒,調用前面提到的Selectable接口獲取Runnable,直接返回給ExecutionStrategy去處理
            Runnable task = processSelected();
            if (task != null)
                return task;
             
           // 如果沒有I/O事件就緒,就干點雜活,看看有沒有客戶提交了更新Selector的任務,就是上面提到的SelectorUpdate任務類。
            processUpdates();
            updateKeys();
 
           // 繼續執行select方法,偵測I/O就緒事件
            if (!select())
                return null;
        }
    }
 }

SelectorProducer 是 ManagedSelector 的內部類。

SelectorProducer 實現了 ExecutionStrategy 中的 Producer 接口中的 produce 方法,需要向 ExecutionStrategy 返回一個 Runnable。

在 produce 方法中 SelectorProducer 主要干了三件事:

  1. 如果 Channel 集合中有 I/O 事件就緒,調用前面提到的 Selectable 接口獲取 Runnable,直接返回給
    ExecutionStrategy 處理。
  2. 如果沒有 I/O 事件就緒,就干點雜活,看看有沒有客戶提交了更新 Selector 上事件注冊的任務,也就是上面提到的
    SelectorUpdate 任務類。
  3. 干完雜活繼續執行 select 方法,偵測 I/O 就緒事件。

3.2 Jetty 的線程執行策略

3.2.1 ProduceConsume(PC) 線程執行策略

任務生產者自己依次生產和執行任務,對應到 NIO 通信模型就是用一個線程來偵測和處理一個 ManagedSelector 上的所有的 I/O 事件。

后面的 I/O 事件要等待前面的 I/O 事件處理完,效率明顯不高。


圖片


圖中,綠色代表生產一個任務,藍色代表執行這個任務,下同。

3.2.2 ProduceExecuteConsume(PEC) 線程執行策略

任務生產者開啟新線程來執行任務,這是典型的 I/O 事件偵測和處理用不同的線程來處理。

缺點是不能利用 CPU 緩存,并且線程切換成本高。

圖片

圖中,棕色代表線程切換,下同。

3.2.3 ExecuteProduceConsume(EPC) 線程執行策略

任務生產者自己運行任務,這種方式可能會新建一個新的線程來繼續生產和執行任務。

它的優點是能利用 CPU 緩存,但是潛在的問題是如果處理 I/O 事件的業務代碼執行時間過長,會導致線程大量阻塞和線程饑餓。

圖片

3.2.4 EatWhatYouKill(EWYK) 改良線程執行策略

這是 Jetty 對 ExecuteProduceConsume 策略的改良,在線程池線程充足的情況下等同于 ExecuteProduceConsume;

當系統比較忙線程不夠時,切換成 ProduceExecuteConsume 策略。

這么做的原因是:

ExecuteProduceConsume 是在同一線程執行 I/O 事件的生產和消費,它使用的線程來自 Jetty 全局的線程池,這些線程有可能被業務代碼阻塞,如果阻塞的多了,全局線程池中線程自然就不夠用了,最壞的情況是連 I/O 事件的偵測都沒有線程可用了,會導致 Connector 拒絕瀏覽器請求。

于是 Jetty 做了一個優化

在低線程情況下,就執行

ProduceExecuteConsume 策略,I/O 偵測用專門的線程處理, I/O 事件的處理扔給線程池處理,其實就是放到線程池的隊列里慢慢處理。

四、總結

本文基于 Jetty-9 介紹了 ManagedSelector 和 ExecutionStrategy 的設計實現,介紹了 PC、PEC、EPC 三種線程執行策略的差異,從 Jetty 對線程執行策略的改良操作中可以看出,Jetty 的線程執行策略會優先使用 EPC 使得生產和消費任務能夠在同一個線程上運行,這樣做可以充分利用熱緩存,避免調度延遲。

這給我們做性能優化也提供了一些思路:

  1. 在保證不發生線程饑餓的情況下,盡量使用同一個線程生產和消費可以充分利用 CPU 緩存,并減少線程切換的開銷。
  2. 根據實際場景選擇最適合的執行策略,通過組合多個子策略也可以揚長避短達到1+1>2的效果。

參考文檔:

  1. Class EatWhatYouKill
  2. Eat What You Kill
  3. Thread Starvation with Eat What You Kill
責任編輯:龐桂玉 來源: vivo互聯網技術
相關推薦

2009-07-16 09:54:44

LookupEventSwing線程

2024-12-02 10:04:04

2009-07-09 18:16:33

MyEclipse優化

2014-08-13 10:41:08

linux線程

2011-10-13 09:44:49

MySQL

2011-05-30 10:36:49

MySQL

2013-10-16 15:36:53

iOS優化

2009-08-21 11:31:59

異步和多線程的區別

2011-06-24 11:03:31

Qt 多線程 線程

2009-06-11 17:03:29

Java線程

2011-06-24 11:12:39

Qt 多線程 線程

2009-07-11 10:47:15

綜合布線設計寫字樓

2010-06-12 14:59:34

IBM工作負載

2011-12-20 21:12:46

用戶體驗

2010-07-14 09:01:07

架構設計

2011-07-18 18:01:34

buffer cach

2013-07-12 15:17:22

BGP組網BGP協議

2009-07-03 17:18:34

Servlet多線程

2009-07-14 10:13:38

MyEclipse優化

2022-03-02 11:13:50

Web前端開發
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品视频一二三区 | 一二三四在线视频观看社区 | av一二三区 | 午夜在线小视频 | 日韩一区二区av | 国产乱码精品一区二区三区中文 | www.99热这里只有精品 | 亚洲 欧美 另类 日韩 | 日韩在线成人 | 91在线电影 | 日韩在线观看网站 | 午夜爱爱网 | 婷婷在线视频 | 伊人热久久| 草草草草视频 | 不卡一区| 在线永久看片免费的视频 | 日韩精品一区二区三区在线观看 | 99福利视频导航 | 亚洲男人的天堂网站 | 天天av天天好逼 | 激情亚洲 | 国产精品久久久久久久久久久免费看 | 国产精品入口麻豆www | 国产精品久久久久久中文字 | 国产一级片| 国产精品久久视频 | 国产成人免费视频 | 特黄色毛片| 久久久久综合 | 91九色视频在线 | 亚洲激情视频在线 | 一级黄色毛片免费 | 成人毛片网 | 亚洲第一黄色网 | 亚洲一区高清 | 久久国内精品 | 欧美精品一区三区 | 激情五月婷婷 | 亚洲精品久久久久久久久久吃药 | 999精品在线 |