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
- 550