# How LibCore's EventLoop Manages Concurrency and Asynchronous Operations in Ladybird

> Discover how Ladybird's LibCore event loop efficiently manages concurrency and async operations using poll multiplexing lock-free queues and thread-local data, avoiding unnecessary thread spawns.

- Repository: [Ladybird/ladybird](https://github.com/LadybirdBrowser/ladybird)
- Tags: internals
- Published: 2026-03-05

---

**Ladybird's LibCore implements a per-thread event loop using `poll()`-driven multiplexing, lock-free cross-thread queues, and mutex-protected thread-local data to handle timers, I/O notifiers, and deferred work without spawning OS threads for each task.**

The LadybirdBrowser/ladybird project relies on LibCore's EventLoop system to drive all asynchronous operations—from GUI updates to network I/O—using a portable, single-threaded concurrency model. This architecture ensures that **LibCore EventLoop concurrency and asynchronous operations** remain efficient by multiplexing multiple event sources through a single blocking primitive per thread, avoiding the overhead of thread-per-task patterns.

## Architecture Overview

LibCore enforces a **one event loop per thread** design. Each thread that processes events owns a `Core::EventLoopImplementation` instance, created lazily upon first construction of `Core::EventLoop`. Thread-local isolation is maintained through `ThreadData`, a structure holding the poll set, timer heap, notifiers, and a **wake pipe** used to interrupt `poll()` when external threads inject work.

Concurrency safety is achieved through four primary mechanisms:

- **Mutexes** protecting modifications to per-thread `ThreadData` (timer and notifier registration)
- **Atomic flags** (`is_being_deleted`, `is_scheduled`) preventing race conditions during timer cancellation
- **RWLocks** guarding global signal-handler tables
- **Lock-free queues** (`ThreadEventQueue`) enabling cross-thread event posting without blocking the sender

## Core Components and Source Files

### EventLoop and EventLoopImplementation

The public API resides in [`Libraries/LibCore/EventLoop.h`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibCore/EventLoop.h). The `Core::EventLoop` class provides methods like `exec()`, `pump()`, `quit()`, and `deferred_invoke()`. It delegates platform-specific behavior to `Core::EventLoopImplementation`, an abstract base defined in [`Libraries/LibCore/EventLoopImplementation.h`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibCore/EventLoopImplementation.h).

### EventLoopImplementationUnix and ThreadData

The Unix-specific implementation in [`Libraries/LibCore/EventLoopImplementationUnix.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibCore/EventLoopImplementationUnix.cpp) contains the core logic. It maintains a `ThreadData` structure (thread-local) aggregating:

- `TimeoutSet timeouts` – a binary heap managing timer deadlines
- `Vector<Notifier*> notifiers` – file descriptors to monitor
- `Vector<pollfd> poll_fds` – the aggregate poll set including the wake pipe
- Wake pipe file descriptors and a protective mutex

The constructor captures the write end of the wake pipe:

```cpp
EventLoopImplementationUnix::EventLoopImplementationUnix()
    : m_wake_pipe_write_fd(ThreadData::the().wake_pipe_fds[1])
{ }

```

### ThreadEventQueue

Defined in [`Libraries/LibCore/ThreadEventQueue.h`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibCore/ThreadEventQueue.h), this lock-free queue allows any thread to enqueue a `Core::Event` or deferred lambda for a target thread's loop. When posting to another thread, the implementation automatically triggers `wake()` to break that thread's current `poll()` wait.

### Notifier and Timer Classes

`Core::Notifier` (in [`Libraries/LibCore/Notifier.h`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibCore/Notifier.h)) wraps a file descriptor and event mask (read/write/hang-up/error). `Core::Timer` (in [`Libraries/LibCore/Timer.h`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibCore/Timer.h)) provides a convenience wrapper around `EventLoop::register_timer()`, managing the underlying `EventLoopTimer` objects stored in the thread-local `TimeoutSet`.

## The Event Loop Life Cycle

### 1. Construction and Initialization

When a thread first creates a `Core::EventLoop`, the system instantiates `EventLoopImplementationUnix`, which initializes the wake pipe and registers the thread-local data store.

### 2. The Exec Loop

The `exec()` method (in [`Libraries/LibCore/EventLoop.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibCore/EventLoop.cpp)) enters an infinite loop calling `pump()` until `quit()` is requested:

```cpp
int EventLoop::exec() {
    for (;;) {
        if (m_exit_requested) return m_exit_code;
        pump(PumpMode::WaitForEvents);
    }
}

```

### 3. Pumping Events

The `pump(PumpMode mode)` method delegates to the global `EventLoopManagerUnix`:

```cpp
size_t EventLoopImplementationUnix::pump(PumpMode mode) {
    static_cast<EventLoopManagerUnix&>(EventLoopManager::the()).wait_for_events(mode);
    return ThreadEventQueue::current().process();
}

```

### 4. Waiting for Events

`EventLoopManagerUnix::wait_for_events()` performs the following sequence:

1. **Prepare timers** – Converts relative timeouts to absolute times via `absolutize_relative_timeouts()`
2. **Compute blocking timeout** – Determines the earliest timer expiry for the `poll()` timeout
3. **Poll** – Calls `System::poll()` on the aggregated `poll_fds` (wake pipe + notifiers)
4. **Handle wake pipe** – Reads integers from the pipe; `0` indicates a standard wake, while non-zero values represent POSIX signal numbers to dispatch
5. **Dispatch notifiers** – For each ready file descriptor, posts `Core::Event::Type::NotifierActivation` to the `ThreadEventQueue`
6. **Fire timers** – Calls `thread_data.timeouts.fire_expired()` to extract elapsed timers from the heap and post `Event::Type::Timer` events

### 5. Cross-Thread Wake

Any thread may interrupt the loop via `wake()`, which writes to the wake pipe:

```cpp
void EventLoopImplementationUnix::wake() {
    int wake_event = 0;
    auto result = Core::System::write(m_wake_pipe_write_fd,
                                     { &wake_event, sizeof(wake_event) });
    // Ignore EBADF when the thread is shutting down.
}

```

## Asynchronous Primitives

LibCore provides five primary mechanisms for asynchronous work:

- **Deferred Invoke** – Enqueues a lambda into the current thread's `ThreadEventQueue`. When called from a foreign thread, it automatically triggers `wake()` to break the poll. Usage: `Core::deferred_invoke([] { /* UI update */ });`

- **Timers** – `EventLoop::register_timer()` creates an `EventLoopTimer` stored in the thread's `TimeoutSet`. The poll loop monitors expiry and fires callbacks. Usage: `int timer_id = Core::EventLoop::register_timer(receiver, 200, true);`

- **Notifiers** – `Notifier` objects register file descriptors with the poll set. Ready notifications become `Core::Event::Type::NotifierActivation` events. Usage: `Notifier noti(fd, Notifier::Read);`

- **Signals** – `register_signal()` installs a handler that writes the signal number into the wake pipe. The poll loop later transforms this into a dispatched callback on the event loop thread.

- **Cross-Thread Events** – `ThreadEventQueue::post_event(target, event_type)` pushes events across thread boundaries, processed after the next wake-up.

## Concurrency Safety Mechanisms

**Thread-local isolation** ensures that timers, notifiers, and wake pipes are owned by a single thread, eliminating data races when two threads manipulate their own loops concurrently.

**Mutex protection** guards all modifications to a thread's `ThreadData`. Adding or removing timers and notifiers requires acquiring `thread_data.mutex`.

**Atomic deletion flags** prevent use-after-free during timer cancellation. When a timer is cancelled, `is_being_deleted` is set atomically, preventing the event loop from dispatching a deleted timer while another thread might hold a reference.

**Signal table RWLock** (`s_thread_data_lock`) allows concurrent reads of the global signal handler map while safely serializing updates.

## Practical Implementation Examples

### Running a Simple Event Loop

```cpp
// main.cpp
#include <LibCore/EventLoop.h>
#include <LibCore/Timer.h>
#include <LibCore/DeferredInvocation.h>

int main()
{
    Core::EventLoop loop;

    // Fire a message after 1 second, then quit.
    Core::Timer::single_shot(1000, [] {
        dbgln("Timer fired – exiting");
        Core::EventLoop::current().quit(0);
    });

    // Schedule a deferred lambda from the same thread.
    Core::deferred_invoke([] {
        dbgln("Deferred work executed before the first poll");
    });

    return loop.exec();   // Blocks until quit() is called.
}

```

### Watching a Socket with a Notifier

```cpp
#include <LibCore/EventLoop.h>
#include <LibCore/Notifier.h>
#include <sys/socket.h>

int main()
{
    int sv[2];
    socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
    Core::Notifier read_notifier(sv[0], Core::Notifier::Read);

    read_notifier.on_ready = [&](auto&) {
        char buf[64];
        ssize_t n = read(sv[0], buf, sizeof(buf));
        dbgln("Received {} bytes", n);
    };

    Core::EventLoop loop;
    return loop.exec();
}

```

### Cross-Thread Wake and Deferred Invoke

```cpp
#include <LibCore/EventLoop.h>
#include <LibThread/Thread.h>

void worker_thread(Core::EventLoop& loop)
{
    // Simulate work, then ask the main loop to quit.
    sleep(2);
    Core::deferred_invoke([&] {
        dbgln("Worker asks main loop to quit");
        loop.quit(0);
    });
}

int main()
{
    Core::EventLoop loop;
    Thread::create([&] { worker_thread(loop); });
    return loop.exec();
}

```

## Summary

- LibCore provides **one event loop per thread**, eliminating shared-state complexity across threads
- The **wake pipe mechanism** (implemented in [`EventLoopImplementationUnix.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/EventLoopImplementationUnix.cpp)) allows instantaneous interruption of `poll()` for cross-thread work injection
- **Lock-free `ThreadEventQueue`** enables safe, non-blocking event posting between threads
- **Binary heap timer management** (`TimeoutSet`) provides O(log n) insertion and O(1) expiry retrieval for high-frequency timer operations
- **Mutex-protected `ThreadData`** and atomic flags ensure safe concurrent modification of event sources without coarse-grained global locks

## Frequently Asked Questions

### How does LibCore handle cross-thread event posting?

LibCore uses the lock-free `ThreadEventQueue` class to post events across threads. When `ThreadEventQueue::post_event()` is called from a foreign thread, it enqueues the event and automatically invokes `wake()` on the target thread's event loop, writing to the wake pipe to interrupt the current `poll()` wait and ensure immediate processing.

### What mechanism interrupts the poll() wait when new events arrive?

The system uses a **wake pipe**, a standard Unix pipe registered in the poll set. When `wake()` is called (either explicitly or automatically during cross-thread posting), the implementation writes an integer to the pipe. The `EventLoopManagerUnix::wait_for_events()` function detects this read-ready state and processes the wake event before returning to the caller.

### How are timers managed in the EventLoop?

Timers are stored in a per-thread `TimeoutSet` (a binary heap) inside `ThreadData`. When `register_timer()` is called, an `EventLoopTimer` object is allocated and inserted into the heap. During `wait_for_events()`, the loop calculates the earliest expiry time to set the `poll()` timeout, then calls `fire_expired()` to dispatch callbacks for elapsed timers.

### Is the EventLoop thread-safe for multiple threads accessing the same loop?

No, individual `EventLoop` instances are **not** thread-safe for concurrent access by multiple threads. Each thread must own its own loop. However, LibCore provides thread-safe mechanisms to *interact with* a foreign loop: `ThreadEventQueue` for posting events, `wake()` for interruption, and atomic flags for timer cancellation. All direct manipulation of a loop's timers and notifiers must occur on that loop's own thread or be protected by the `ThreadData` mutex.