The Main Loop

2019

Asynchronous programming in UNIX

Recap: Embedded

bool queue_pop(struct queue *q, struct event *e);
void queue_push(struct queue *q, struct event *e);

struct queue queue;

while (true)
{
    struct event;

    if (!queue_pop(&queue, &event))
    {
        __WFE();
        continue;
    }

    some_task(&event);
    another_task(&event);
}

wait for anything

handle everything

Recap: UNIX

int connection = socket(...);

fd_set descriptors;

while (true)
{
    FD_ZERO(&descriptors);

    FD_SET(connection, &descriptors);


    select(&descriptors);



    if (FD_ISSET(connection, &descriptors))
        process_connection();
}

wait for "interesting things"

handle things that  happened

Reactor pattern

Dispatcher

add(handler)
remove(handler)
dispatch()

Demultiplexer

select()

uses

Handler

handle(type)

calls

Descriptor

owns

notifies

Douglas C. Schmidt, 1995

Reactor: handler

class Handler:
    def __init__(self):
        self.descriptor = ...

    def handle(self):
        ...

Reactor: dispatcher

import select


class Dispatcher:
    def __init__(self):
        self.handlers = {}

    def add(self, handler):
        self.handlers[handler.descriptor] = handler

    def remove(self, handler):
        self.handlers.pop(handler.descriptor)

    def dispatch(self):
        with select.poll() as demultiplexer:
            for descriptor, handler in self.handlers.items():
                demultiplexer.register(descriptor, select.EPOLLIN)

            for descriptor, event in demultiplexer.select():

                self.handlers[descriptor].handle_read()

block

do NOT block

Reactor: demultiplexer

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

Everything is a file

#include <sys/socket.h>

// readable when client connects
int bind(int sockfd, const struct sockaddr *addr,
         socklen_t addrlen);

// writable when server accepts
int connect(int socket, const struct sockaddr *address,
            socklen_t address_len);

Everything is a file

#include <sys/timerfd.h>

// readable when timeout expires
int timerfd_create(int clockid, int flags);

int timerfd_settime(int fd, int flags,
                    const struct itimerspec *new_value,
                    struct itimerspec *old_value);

int timerfd_gettime(int fd, struct itimerspec *curr_value);

Everything is a file

#include <sys/signalfd.h>

// readable when signal arrives
int signalfd(int fd, const sigset_t *mask, int flags);

Everything is a file

#include <sys/inotify.h>

// readable when file changes on disk
int inotify_init(void);

Handlers

class FileHandler:
    def __init__(self, filename):
        self.descriptor = open(filename, 'r')

    def handle_read(self):
        data = self.descriptor.read()


class SocketHandler:
    def __init__(self, address):
        self.descriptor = connect(address)

    def handle_read(self):
        data = self.descriptor.read()


class TimerHandler:
    def __init__(self, timeout):
        self.descriptor = timerfd(timeout)

    def handle_read(self):
        data = self.descriptor.read()

# ...

Handlers: flow control

class Handler:
    READ_BUFFER_SIZE = WRITE_BUFFER_SIZE = 1024

    def __init__(self):
        self.descriptor = ...
        self.read_buffer = b''
        self.write_buffer = b''

    def should_write(self):
        return self.write_buffer

    def handle_write(self):
        write_size = self.descriptor.write(self.write_buffer)
        self.write_buffer = self.write_buffer[write_size:]

    def should_read(self):
        return len(self.read_buffer) < self.READ_BUFFER_SIZE

    def handle_read(self):
        read_size = self.READ_BUFFER_SIZE - len(self.read_buffer)
        self.read_buffer += self.descriptor.read(read_size)

Dispatcher: flow control

class Dispatcher:
    def __init__(self):
        self.handlers = {}

    def dispatch(self):
        with select.poll() as demultiplexer:
            for descriptor, handler in handlers.items():
                if handler.should_read():
                    demultiplexer.register(descriptor, select.EPOLLIN)
    
                if handler.should_write():
                    demultiplexer.register(descriptor, select.EPOLLOUT)

            for descriptor, event in demultiplexer.select()
                if event == select.EPOLLIN:
                    self.handlers[descriptor].handle_read()
    
                if event == select.EPOLLOUT:
                    self.handlers[descriptor].handle_write()

The Main Loop

class MainLoop:
    def __init__(self):
        self.dispatcher = Dispatcher()
        self.running = False

    def run(self):
        self.running = True

        while self.running:
            self.dispatcher.dispatch()

    def quit(self):
        self.running = False

Example: limiting throughput

class Reader:
    MAX_TOKENS = 4096  # allow 'bursts'

    def __init__(self):
        self.descriptor = ...
        self.read_buffer_size = 0
        self.read_buffer = b''

    def add_tokens(self, tokens):
        self.tokens = max(self.tokens + tokens,
                          self.MAX_TOKENS)

    def should_read(self):
        return self.tokens > 0

    def handle_read(self):
        read_buffer = \
            self.descriptor.read(self.tokens)
        self.read_buffer += read_buffer
        self.tokens -= len(read_buffer)
class TokenBucket:
    def __init__(self, reader,
                 interval, tokens):
        self.descriptor = \
            timerfd(interval)
        self.reader = reader
        self.tokens = tokens

    def should_read(self):
        return True

    def handle_read(self):
        self.reader.add_tokens(self.tokens)


reader = Reader()
token_bucket = \
    TokenBucket(reader, 1.0, 1024)

loop.add(reader)
loop.add(token_bucket)

Thank you

Questions?

Main Loop vol 1, unix @ Silvair

By Michał Lowas-Rzechonek

Main Loop vol 1, unix @ Silvair

Asynchronous programming in an embedded environment

  • 526