【JAVA新生】拿協程開始寫個非同步io應用

taowen發表於2014-09-29

前面已經準備好了greenlet對應的Java版本了,一個刪減後的kilim(http://segmentfault.com/blog/taowen/1190000000697487)。接下來,就看怎麼用協程來實現非同步io了。首先,拿一段最最簡單的tcp socket accept的程式碼:

Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9090));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("listening...");
selector.select();
scheduler.accept(serverSocketChannel);
System.out.println("hello");

這裡使用的是java 6的NIO1的selector模型。直接拿NIO的原始api寫程式碼會死人的。引入協程就是為了把上下連續的業務邏輯放在一個協程裡,把與業務關係不大的selector的處理部分放到框架的ioloop裡。也就是把一段交織的程式碼,分成兩個關注點不同的組成部分。
改造之後的程式碼在這裡: https://github.com/taowen/daili/tree/1e319f929678213a8d8f63ee5e8b8cf016637317
這是改造之後的效果:

Scheduler scheduler = new Scheduler();
DailiTask task = new DailiTask(scheduler) {
    @Override
    public void execute() throws Pausable, Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(9090));
        serverSocketChannel.configureBlocking(false);
        System.out.println("listening...");
        scheduler.accept(serverSocketChannel);
        System.out.println("hello");
    }
};
scheduler.callSoon(task);
scheduler.loop();

其中最關鍵的一行是 scheduler.accept(serverSocketChannel); 這個呼叫是阻塞的。但是隻阻塞呼叫它的Task協程。如果有多個Task並行的話,別的Task可以在這個時候被執行。那麼scheduler.accept是如何做到把NIO的selector api轉換成這樣的形式的呢?

public SocketChannel accept(ServerSocketChannel serverSocketChannel) throws IOException, Pausable {
    SocketChannel socketChannel = serverSocketChannel.accept();
    if (null != socketChannel) {
        return socketChannel;
    }
    SelectionKey selectionKey = serverSocketChannel.keyFor(selector);
    if (null == selectionKey) {
        selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, new WaitingSelectorIO());
    } else {
        selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_ACCEPT);
    }
    WaitingSelectorIO waitingSelectorIO = (WaitingSelectorIO) selectionKey.attachment();
    waitingSelectorIO.acceptBlockedAt = System.currentTimeMillis();
    waitingSelectorIO.acceptTask = (Task) Task.getCurrentTask();
    selectionKey.attach(waitingSelectorIO);
    Task.pause(waitingSelectorIO);
    return serverSocketChannel.accept();
}

這個函式分成四部分:第一部分是嘗試去accept,如果有戲就不用NIO了。第二部分是註冊selection key,說明我希望知道什麼時候可以accept了,並把task作為附件加上去。第三部分是Task.pause放棄掉執行權。第四部分是task被回撥了,說明等待的accept已經ok了,可以去呼叫了。
但是Task.pause了之後,是誰在把這個暫停的task重新拉起來執行的呢?這個就是scheduler的loop乾的活了

public void loop() throws IOException {
    while (true) {
        executeReadyTasks();
        selector.select();
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        for (SelectionKey selectionKey : selectionKeys) {
            WaitingSelectorIO waitingSelectorIO = (WaitingSelectorIO) selectionKey.attachment();
            if (selectionKey.isAcceptable()) {
                Task taskToCall = waitingSelectorIO.acceptTask;
                waitingSelectorIO.acceptBlockedAt = 0;
                waitingSelectorIO.acceptTask = null;
                callSoon(taskToCall);
            }
        }
    }
}

在迴圈中呼叫selector.select獲得網路事件的通知。如果selection key就緒了,就把附件裡的task取出來回撥。具體的回撥發生在executeReadyTasks內部,其實就是呼叫一下resume而已。

private void executeReadyTasks() {
    Task task;
    while((task = readyTasks.poll()) != null) {
        executeTask(task);
    }
}

private void executeTask(Task task) {
    try {
        task.resume();
    } catch (Exception e) {
        LOGGER.error("failed to execute task: " + task, e);
    }
}

這樣一個只能接收telnet 127.0.0.1 9090列印一行hello的非同步io應用就寫好了。

相關文章