構建一個基於事件分發驅動的EventLoop執行緒模型

bigfan發表於2020-07-27

在之前的文章中我們詳細介紹過Netty中的NioEventLoop,NioEventLoop從本質上講是一個事件迴圈執行器,每個NioEventLoop都會繫結一個對應的執行緒通過一個for(;;)迴圈來處理事件訊息。今天我們就借鑑NioEventLoop,並加入訊息分發策略構建一個基礎的Eventloop執行緒模型。

整個執行緒模型可以劃分為三部分

事件監聽及分發(Event):  監聽訊息事件並通過分發器分發;

分發器(Dispatch):將訊息按照分發策略分發到對應的事件迴圈器中;

事件迴圈器 (EventLoop) :繫結一個對應的執行緒與訊息佇列,迴圈處理訊息;

模型圖如下所示

 

事件監聽器把監聽到的事件交給分發器進行分發,分發器根據一定的分發策略把訊息事件分發至對應的Eventloop的訊息事件佇列中,每個EventLoop內部會啟動一個執行緒輪詢佇列中的訊息事件並進行處理;

下面結合程式碼看下具體實現

一、事件監聽及分發

首先是事件監聽機制,定義了事件、事件源、事件監聽器三部分

/** 
 * 事件類,用於封裝事件源及一些與事件相關的引數. 
 */ 
public class LoopEvent extends EventObject {
    
    private static final long serialVersionUID = 1L;  

    public LoopEvent(Object source) {
        super(source);
        // TODO Auto-generated constructor stub
    }

}

/**
 * 事件源
 *
 */
public class LoopEventSource {
    // 監聽器容器
    private static Set<AbstractEventListener> listeners;

    public LoopEventSource() {
        listeners = new HashSet<AbstractEventListener>();
    }

    // 給事件源註冊監聽器
    public void addEventListener(AbstractEventListener listener) {
        listeners.add(listener);
    }

    // 當事件發生時,通知註冊在該事件源上的所有監聽器做出相應的反應(呼叫回撥方法)
    public void notifies(LoopEvent event) {
        AbstractEventListener listener = null;
        try {
            Iterator<AbstractEventListener> iterator = listeners.iterator();
            while (iterator.hasNext()) {
                listener = iterator.next();
                listener.fireEvent(event); // 如果事件訊息型別不同,可以進行封裝
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

/**
 * 事件監聽器
 *
 */
public class LoopEventListener extends AbstractEventListener {
    
    EventLoopDispatch<String,String> dispatcher = new EventLoopDispatch<String,String>(4);//定義及初始化一個訊息分發器

    @Override
    public void fireEvent(LoopEvent e) {
        EventRecord<String,String> eventRecord = new EventRecord<String, String>(null, e.getSource().toString());//轉為內部統一的訊息型別
        dispatcher.dispatch(eventRecord);//分發訊息事件
    }

}

以上程式碼就是實現一個簡單的事件監聽機制,需要注意兩點:

1、事件監聽器中對訊息分發器的定義及初始化,因為在分發器中會同時構造及初始化EventLoopGroup與DefaultPartitioner,前者顧名思義是指一組事件迴圈器,後者則是分發策略的具體實現;

2、為了配置分發策略需要把事件資料轉為執行緒模型內部統一的訊息型別;

二、分發器(Dispatch)

接收到事件觸發後,我們會把事件轉為統一的內部訊息型別,然後交給dispatch進行分發,來看下分發的具體程式碼。

    
    //訊息分發與執行
    public void dispatch(EventRecord<K, V> record) {
        int partation = partitioner.partition(core,record.partition(), record.key());//根據傳入的訊息資料,確定partation
        group.next(partation, record);//根據partation,確定執行的EventLoop
    }

分發方法實現的功能也很簡單,第一步根據傳入的訊息體資料拿到具體的partation,第二步把拿到的partation與訊息資料傳入group的next方法,分配指定的EventLoop;

在AbstractDispatch建構函式中對EventLoopGroup與DefaultPartitioner進行初始化操作;

    protected AbstractDispatch(int core) {
        this.core = core;
        this.partitioner = new DefaultPartitioner();
        this.group = new ThreadEventLoopGroup(core);
    }

DefaultPartitioner類借鑑了Kafka Consumer的實現,主要實現partitioner的分配策略,可以通過指定訊息實體EventRecord的Key或Partition,保證訊息資料按照一定的規則落入對應的EventLoop中。

根據EventRecord訊息構造的不同,三種策略如下:

1、構造EventRecord時,確定了partition,判斷後直接返回;

2、構造EventRecord時,Key與partition均為空,自增取餘;

3、構造EventRecord時,partition為空,Key不為空,hash取餘;

public class DefaultPartitioner implements Partitioner {

    private final ConcurrentMap<Integer, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();

    public int partition(int core, Integer partition, Object key) {
        if (partition != null ) {
            if(partition>core) {//可以自己指定partition,但不能大於core,也就是group個數
                throw new IllegalArgumentException(String
                        .format("Invalid partition: %d. partition should always be greater than core.", partition));
            }
            return partition;
        } else if (partition == null && key == null) {//如果沒有指定key或partition,則採用自增取餘模式
            int nextValue = nextValue(core);
            return ConstantUtil.toPositive(nextValue) % core;

        } else {//如果partition為空,key不為空,則根據hash取餘
            return Math.abs(key.hashCode() % core);
        }
    }

    private int nextValue(int core) {
        AtomicInteger counter = topicCounterMap.get(core);
        if (null == counter) {
            counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
            AtomicInteger currentCounter = topicCounterMap.putIfAbsent(core, counter);
            if (currentCounter != null) {
                counter = currentCounter;
            }
        }
        return counter.getAndIncrement();
    }
}

而ThreadEventLoopGroup 顧名思義就是維護一組EventLoop,會根據你傳入的引數大小建立並初始化一個EventLoop陣列;next方法會根據傳入的partition,把訊息資料交給EventLoop陣列中的一個來執行

public class ThreadEventLoopGroup {

    private ThreadEventLoop[] children;

    public ThreadEventLoopGroup(int threads) {
        this(threads, null);
    }

    public ThreadEventLoopGroup(int threads, Executor executor) {
        if (executor == null) {//自定義執行緒工廠
            executor = new ThreadPerTaskExecutor(EventThreadFactory());
        }
        //根據傳入的執行緒數建立一個EventLoop陣列
        children = new ThreadEventLoop[threads];

        //初始化EventLoop
        for (int i = 0; i < threads; i++) {
            children[i] = new ThreadEventLoop(executor);
        }
    }

    protected ThreadFactory EventThreadFactory() {
        return new EventThreadFactory();
    }

    //分配EventLoop
    public void next(int partaion, EventRecord<?, ?> context) { 
        children[partaion].execute(context);
    }

}

以上部分主要實現了訊息資料的接收與分發,具體分發策略由DefaultPartitioner與 EventRecord相互配合實現  ,使用者既可以通過構造EventRecord自定義分發策略,也可以使用預設分發策略;

三、事件迴圈器 (EventLoop)

訊息經過分發器分發後,會進入對應EventLoop,我們首先看下EventLoop及其抽象類的實現

ThreadEventLoop類

public class ThreadEventLoop extends AbstractEventLoop {
    
    
    public ThreadEventLoop(Executor executor) {
        super(executor);

    }
    
    public void run() {
        //通過for(;;)輪詢的方式從阻塞佇列中獲取資料進行處理
        for (;;) {
            try {
                EventRecord<?, ?> record = arrayBlockingQueue.take();//從阻塞佇列中獲取資料
                iHandler.execute(record);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                System.err.println(e.getMessage());
            }
        }

    }

    public void execute(EventRecord<?, ?> record) {
        try {
            if (record == null) {//判斷非空
                throw new NullPointerException("EventRecord");
            }

            boolean inEventLoop = inEventLoop(Thread.currentThread());
            arrayBlockingQueue.put(record);//把訊息放入阻塞佇列中
            if (!inEventLoop) {
                startThread();//啟動EventLoop中改的執行緒
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            System.err.println(e.getMessage());
        }
    }



}

 execute作為執行方法入口主要起到兩點作用:一是要把收到的訊息資料放入阻塞佇列中,二是啟動EventLoop中的執行緒,從而執行run方法輪詢佇列進行處理;

在抽象類AbstractEventLoop 中,startThread方法通過CAS的方式,保證了每個EventLoop 中自定義的執行緒工廠只建立和啟動一次執行緒;

public abstract class AbstractEventLoop {

    //阻塞佇列,訊息會先進入阻塞佇列中,再由執行緒迴圈處理
    protected final ArrayBlockingQueue<EventRecord<?,?>> arrayBlockingQueue = new ArrayBlockingQueue<EventRecord<?,?>>(1024);

    //繫結執行緒
    protected volatile Thread thread;

    protected final Executor executor;
    
    protected IHandler iHandler = new EventHandler();

    //宣告AtomicIntegerFieldUpdater類,用於控制執行緒啟動狀態
    private static final AtomicIntegerFieldUpdater<AbstractEventLoop> STATE_UPDATER = AtomicIntegerFieldUpdater
            .newUpdater(AbstractEventLoop.class, "state");

    private volatile int state = ST_NOT_STARTED;
    private static final int ST_NOT_STARTED = 1;
    private static final int ST_STARTED = 2;

    protected AbstractEventLoop(Executor executor) {
        this.executor = executor;
    }

    public boolean inEventLoop(Thread thread) {
        return thread == this.thread;
    }

    //啟動執行緒
    protected void startThread() {
        if (state == ST_NOT_STARTED) {
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {//通過CAS的方式保證執行緒不會重複啟動
                try {
                    doStartThread();//
                } catch (Throwable cause) {
                    STATE_UPDATER.set(this, ST_NOT_STARTED);
                }
            }
        }
    }

    protected void doStartThread() {
        assert thread == null;
        executor.execute(new Runnable() {//通過自定義執行緒工廠開始執行執行緒
            @Override
            public void run() {
                thread = Thread.currentThread();
                AbstractEventLoop.this.run();//執行實現類的run方法

            }
        });
    }
    
    protected abstract void run();
}

可以看到EventLoop要實現的目標很明確,每個EventLoop都會繫結與維護一個對應的執行緒,該執行緒輪詢佇列中的訊息進行處理;

四、總結

以上就是一個由事件訊息驅動的EventLoop執行緒模型的構建,其中涉及到了事件監聽、訊息分發、執行緒輪詢等內容,相比與一般的生產者消費者模型,具備以下特點:

1、結合事件監聽機制,統一內部訊息模型,為分發策略的制定與無鎖化提供基礎;
2、執行緒內部結合阻塞佇列迴圈執行,保證同一EventLoo中資料處理的有序性;
3、針對需要執行緒同步資料,可根據一定的分發策略保證由同一執行緒執行,實現無鎖化;

當然相比Netty等框架中的EventLoop執行緒模型,本文只是基於自己理解實現的簡單例項,其中有很多複雜的細節都未考慮,但仍然希望對大家能有所幫助,其中如有不足與不正確的地方還望指出與海涵。

 

github地址:https://github.com/dafanjoy/fan-eventLoop

 

關注微信公眾號,檢視更多技術文章。

 

 

轉載說明:未經授權不得轉載,授權後務必註明來源(註明:來源於公眾號:架構空間, 作者:大凡)

 

相關文章