Duy trì nhịp sống

Có lẽ tất cả chúng ta ai cũng đã từng chạy chương trình Hello World kinh điển rồi nhỉ? Nó sẽ kiểu thế này:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

Nhưng có 1 điều đáng lưu ý ở đây là, chương trình của chúng ta chỉ in ra đúng 1 dòng Hello World! và kết thúc. Vậy thì tại sao các ứng dụng server, các ứng dụng client chúng ta vẫn đang sử dụng trên máy tính, điện thoại, web, vẫn chạy ầm vậy nhỉ? Câu trả lời đó chính là event loop.

Cấu tạo bên trong

Mỗi chương trình hay ứng dụng đểu sẽ bắt đầu với hàm main, và để hàm main này không bị kết thúc thì chúng ta sẽ phải sử dụng đến event loop. Một chương trình sẽ có tối thiểu một event loop, và một event loop được cấu tạo bởi 2 thành phần:

  1. Một vòng lặp while: vòng lặp này sẽ chạy cho đến khi nào chúng ta gọi hàm stop thì thôi, nó sẽ đảm bảo hàm main sẽ không thể kết thúc được
  2. Một hàng đợi: hàng đợi này để quản lý các event theo thứ tự, cái nào vào trước sẽ được lấy ra thực hiện trước

Hai loại event loop

Có 2 loại event loop chính là nonblocking và blocking, mỗi loại sẽ được sử dụng cho các trường hợp khác nhau dựa vào đặc tính của chúng

Nonblocking event loop

Loại event loop này sử dụng một vòng lặp và một nonblocking queue, source code sẽ kiểu thế này (ví dụ đầy đủ bạn có thể tham khảo tại đây):

public class NonBlockingEventLoop {
    private volatile boolean active;
    private final long sleepTime;
    private Runnable onUpdateCallback;
    private final Queue<Runnable> eventQueue;

    public NonBlockingEventLoop() {
        this.sleepTime = 3;
        this.eventQueue = new LinkedList<>();
    }

    public void addEvent(Runnable event) {
        synchronized (eventQueue) {
            eventQueue.offer(event);
        }
    }

    public void onUpdate(Runnable callback) {
        this.onUpdateCallback = callback;
    }

    public void start() {
        Throwable exception = null;
        Queue<Runnable> buffer = new LinkedList<>();
        this.active = true;
        while (active) {
            try {
                long nextTime = System.currentTimeMillis() + sleepTime;
                synchronized (eventQueue) {
                    while (!eventQueue.isEmpty()) {
                        buffer.add(eventQueue.poll());
                    }
                }
                while (!buffer.isEmpty()) {
                    Runnable event = buffer.poll();
                    event.run();
                }
                if(onUpdateCallback != null) {
                    onUpdateCallback.run();
                }
                long currentTime = System.currentTimeMillis();
                if(currentTime < nextTime) {
                    long offset = nextTime - currentTime;
                    Thread.sleep(offset);
                }
            }
            catch (Throwable e) {
                exception = e;
                active = false;
            }
        }
        if(exception != null) {
            throw new IllegalStateException(exception);
        }
    }

    public void stop() {
        this.active = false;
    }
}

Đặc điểm của nonblocking event loop là nó có Thread.sleep(offset);, vì sao phải sleep vậy? Chúng ta hãy nhìn vào hình phía dưới nhé.

Phía bên tay trái là even loop khi có Thread.sleep và phía bên tay phải là event loop khi không có Thread.sleep. Bạn có thể thấy nếu không có Thread.sleep vòng lặp sẽ chiếm dụng CPU rất nhiều, máy của mình là 8 core là 12%, nên những máy chỉ có 1 core sẽ là 100%. Vậy nên Thread.sleep sẽ là điều bắt buộc.

Phù hợp với

Nonblocking event loop phù hợp với các ứng dụng chạy trên main thread như các ứng dụng client cho desktop, android hay ios. Các framework dành cho client cũng đều render view trên event loop của main thread.

Một số framework dành cho server như nodejs, dart cũng cố gắng tổ chức chương trình với event loop chạy trên mainthread để giúp cho lập trình viên quên đi sự phức tạp của lập trình đa luồng.

Ví dụ khi sử dụng event loop trên main thread (ví dụ đầy đủ bạn có thể xem tại đây):

NonBlockingEventLoop eventLoop = new NonBlockingEventLoop();
Runnable updateViewEvent = new Runnable() {
    @Override
    public void run() {
        System.out.println("update state");
        eventLoop.addEvent(this);
    }
};
eventLoop.addEvent(updateViewEvent);
eventLoop.onUpdate(() -> System.out.println("Update view"));
eventLoop.start();
Ưu điểm

Đảm bảo chương trình của chúng ta luôn luôn chạy thậm chí với chỉ một luồng main duy nhất, từ đó giảm thiểu sự phức tạp của lập trình đa luồng.

Nhược điểm

Phải có sleep time, dù cho có event mới đến nhưng đúng lúc Thread.sleep được gọi thì event đó vẫn phải chờ, đều này làm thời gian thực thi bị sai lệch đi đôi chút.

Blocking Event Loop

Loại event loop này sử dụng một vòng lặp và một blocking queue, source code sẽ kiểu thế này (ví dụ đầy đủ bạn có thể xem tại đây):

public class BlockingEventLoop {
    private volatile boolean active;
    private final BlockingQueue<Runnable> eventQueue;
    private final static Runnable FINISH_EVENT = () -> {};

    public BlockingEventLoop() {
        this(new LinkedBlockingQueue<>());
    }

    public BlockingEventLoop(
        BlockingQueue<Runnable> eventQueue
    ) {
        this.eventQueue = eventQueue;
    }

    public void addEvent(Runnable event) {
        eventQueue.offer(event);
    }

    public void start() {
        Throwable exception = null;
        Queue<Runnable> buffer = new LinkedList<>();
        this.active = true;
        while (active) {
            try {
                Runnable event = eventQueue.take();
                if(event == FINISH_EVENT) {
                    break;
                }
                event.run();
            }
            catch (Throwable e) {
                exception = e;
                active = false;
            }
        }
        if(exception != null) {
            throw new IllegalStateException(exception);
        }
    }

    public void stop() {
        this.active = false;
        this.eventQueue.offer(FINISH_EVENT);
    }
}

Ngược với nonblocking event loop, đặc điểm của block event loop là nó không có Thread.sleep, nhưng nó bị block tại eventQueue.take, nó sẽ chờ cho đến khi nào có sự kiện mới đến thì mới xử lý, điều này giúp nó tiết kiệm được cpu nếu không có event nào trong queue.

Phù hợp với

Blocking event loop phù hợp với các ứng dụng chạy đa luồng trên server, để giảm thiểu tài nguyên xuống mức tối thiểu thì các vòng lặp nên chờ đến khi nào có sự kiện thì mới nên thức dậy để xử lý.

Đa phần các framework trên server đều sử dụng blocking event loop, trong đó có ezyfox-server, nó sử dụng cho việc lắng nghe request từ client, lắng nghe response từ server hay sự kiện user bị disconnect.

Ví dụ khi sử dụng event loop với đa luồng (ví dụ đầy đủ bạn có thể xem tại đây):

ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
BlockingEventLoop eventLoop = new BlockingEventLoop();
scheduler.scheduleAtFixedRate(() -> {
    eventLoop.addEvent(() -> {
        System.out.println("event's processing");
    });
}, 0, 3, TimeUnit.SECONDS);
eventLoop.start();
Ưu điểm
  • Giảm thiểu tối đa tài nguyên CPU khi không có sự kiện nào
  • Đảm bảo sự kiện sẽ được thực thi ngay khi nó được thêm vào queue
Nhược điểm
  • Các hàm ở phía sau eventQueue.take() sẽ không được thực thi cho đến khi có sự kiện mới, điều này khiến chúng ta phải sử dụng thêm 1 luồng nữa để đưa event vào queue
  • Bạn phải quan tâm đến các vấn đề của đa luồng và điều này không dễ chịu cho lắm

Tổng kết

Event loop gần như là 1 thành phần không thể thiếu trong mọi chương trình, có điều là hình thù sẽ khác nhau trên các ngôn ngữ khác nhau mà thôi. Nhưng cơ bản thì event loop sẽ có 2 thành phần là vòng lặp và queue.

Nonblocking event loop sẽ phù hợp hơn cho các ứng dụng client, hay các ứng dụng cần update liên tục sử dụng 1 thread duy nhất. Còn blocking event sẽ phù hợp với các ứng dụng chạy trên server, trong môi trường đa luồng và tiết kiệm tối đa tài nguyên CPU.