自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

淺析 Jetty 中的線程優(yōu)化思路

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

一、什么是 Jetty

Jetty 跟 Tomcat 一樣是一種 Web 容器,它的總體架構(gòu)設(shè)計如下:

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

圖片

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

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

二、Jetty 中的 Selector 交互

2.1 傳統(tǒng)的 Selector 實現(xiàn)

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

具體過程是:

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

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

這兩個線程是"生產(chǎn)者"和"消費者"的關(guān)系。

這樣設(shè)計的好處:

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

這樣設(shè)計的缺陷:

當 Selector 檢測讀就緒事件時,數(shù)據(jù)已經(jīng)被拷貝到內(nèi)核中的緩存了,同時 CPU 的緩存中也有這些數(shù)據(jù)了。

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

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

2.2 Jetty 中的 ManagedSelector 實現(xiàn)

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

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

ManagedSelector 本質(zhì)上是一個 Selector,負責 I/O 事件的檢測和分發(fā)。

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

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

2.2.1 SelectorUpdate 接口

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

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

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

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

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

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

ManagedSelector 做的操作。

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

它就向 ManagedSelector 提交了一個內(nèi)部任務(wù)類

ManagedSelector.SelectorUpdate:

_selector.submit(_updateKeyAction);

這個 _updateKeyAction 就是一個

SelectorUpdate 實例,它的 update 方法實現(xiàn)如下:

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

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

2.2.2 Selectable 接口

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

它在一個死循環(huán)里拉取這些 SelectorUpdate 任務(wù)逐個執(zhí)行。

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

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

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

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

然后把 Runnable 扔給線程池去執(zhí)行。

三、Jetty 的線程優(yōu)化思路

3.1 Jetty 中的 ExecutionStrategy 實現(xiàn)

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

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

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

ExecutionStrategy 接口。

這個接口將具體任務(wù)的生產(chǎn)委托給內(nèi)部接口 Producer,而在自己的 produce 方法里實現(xiàn)具體執(zhí)行邏輯。

這個 Runnable 的任務(wù)可以由當前線程執(zhí)行,也可以放到新線程中執(zhí)行。

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

實現(xiàn) Produce 接口生產(chǎn)任務(wù),一旦任務(wù)生產(chǎn)出來,ExecutionStrategy 會負責執(zhí)行這個任務(wù)。

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事件就緒,調(diào)用前面提到的Selectable接口獲取Runnable,直接返回給ExecutionStrategy去處理
            Runnable task = processSelected();
            if (task != null)
                return task;
             
           // 如果沒有I/O事件就緒,就干點雜活,看看有沒有客戶提交了更新Selector的任務(wù),就是上面提到的SelectorUpdate任務(wù)類。
            processUpdates();
            updateKeys();
 
           // 繼續(xù)執(zhí)行select方法,偵測I/O就緒事件
            if (!select())
                return null;
        }
    }
 }

SelectorProducer 是 ManagedSelector 的內(nèi)部類。

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

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

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

3.2 Jetty 的線程執(zhí)行策略

3.2.1 ProduceConsume(PC) 線程執(zhí)行策略

任務(wù)生產(chǎn)者自己依次生產(chǎn)和執(zhí)行任務(wù),對應(yīng)到 NIO 通信模型就是用一個線程來偵測和處理一個 ManagedSelector 上的所有的 I/O 事件。

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


圖片


圖中,綠色代表生產(chǎn)一個任務(wù),藍色代表執(zhí)行這個任務(wù),下同。

3.2.2 ProduceExecuteConsume(PEC) 線程執(zhí)行策略

任務(wù)生產(chǎn)者開啟新線程來執(zhí)行任務(wù),這是典型的 I/O 事件偵測和處理用不同的線程來處理。

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

圖片

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

3.2.3 ExecuteProduceConsume(EPC) 線程執(zhí)行策略

任務(wù)生產(chǎn)者自己運行任務(wù),這種方式可能會新建一個新的線程來繼續(xù)生產(chǎn)和執(zhí)行任務(wù)。

它的優(yōu)點是能利用 CPU 緩存,但是潛在的問題是如果處理 I/O 事件的業(yè)務(wù)代碼執(zhí)行時間過長,會導(dǎo)致線程大量阻塞和線程饑餓。

圖片

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

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

當系統(tǒng)比較忙線程不夠時,切換成 ProduceExecuteConsume 策略。

這么做的原因是:

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

于是 Jetty 做了一個優(yōu)化

在低線程情況下,就執(zhí)行

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

四、總結(jié)

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

這給我們做性能優(yōu)化也提供了一些思路:

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

參考文檔:

  1. Class EatWhatYouKill
  2. Eat What You Kill
  3. Thread Starvation with Eat What You Kill
責任編輯:龐桂玉 來源: vivo互聯(lián)網(wǎng)技術(shù)
相關(guān)推薦

2009-07-16 09:54:44

LookupEventSwing線程

2024-12-02 10:04:04

2009-07-09 18:16:33

MyEclipse優(yōu)化

2011-05-30 10:36:49

MySQL

2011-10-13 09:44:49

MySQL

2014-08-13 10:41:08

linux線程

2009-08-21 11:31:59

異步和多線程的區(qū)別

2013-10-16 15:36:53

iOS優(yōu)化

2011-06-24 11:12:39

Qt 多線程 線程

2011-06-24 11:03:31

Qt 多線程 線程

2009-06-11 17:03:29

Java線程

2011-07-18 18:01:34

buffer cach

2010-07-14 09:01:07

架構(gòu)設(shè)計

2011-12-20 21:12:46

用戶體驗

2022-03-02 11:13:50

Web前端開發(fā)

2009-07-11 10:47:15

綜合布線設(shè)計寫字樓

2010-06-12 14:59:34

IBM工作負載

2009-07-14 10:13:38

MyEclipse優(yōu)化

2009-10-16 10:20:37

Python的GIL

2009-07-03 17:18:34

Servlet多線程
點贊
收藏

51CTO技術(shù)棧公眾號