Reconsidering Event Loops

Again, looking at Node, or any other eventing model, when you break it down what do you get?  I talked about IEnumerable vs IObservable, and the fact that they’re two sides of the same coin. Now I want to dig even deeper and look at the point at which you decide which way to go.  When do you enter ‘callback hell’ and when do you go some different way.

This time, I use epoll on Linux as the foil for my follies.  epoll is an eventing system which evolved from the archaic select, and poll methods of eventing on Linux.  We’re talking the beginning of time with select(), and more modern times with poll.  The basic problem this system tries to solve is this.  You have a file descriptor (network sockets are also file descriptors in Linux), and you want to know when it’s safe to read from that descriptor without blocking.  You want to know this for a few reasons.

If you have any hope of writing a highly performant, responsive, and scalable application, you don’t want to sit around waiting for things to happen.  Imagine if all your graphic rendering halted while you waited for a network packet to arrive, or waited for the user to press a key on the keyboard, or move their mouse.  Rather than wait around, you’d like to to be notified if any of those events occur, otherwise, you’ll want to continue on with other stuff, like drawing graphics.  Your application should have a basic event loop, that looks something like this:

 

while continue do
  if EventsAreReady() then
    HandleEvents()
  end

  PerformIdleOperations()
end

And that’s about it. All exotic forms of libraries from games to massively scalable web servers break down to this fundamental while loop, or something very similar.

What types of events are we talking about? In the case of file descriptors, let’s examine the keyboard. The keyboard can be represented by a particular file, such as “/dev/input/event0”. If you read from this file, you get successive data structures of the form ‘input_event’. Each time you press or release a key on the keyboard, you’ll get exactly 3 such event structures. At the point when you know a key has been pressed, you might write the following code:

input_event keyactivity[3];
int bytesread = read(keyboardfd, keyactivity, sizeof(input_event)*3);

At that point, you would have all the information you need to decode which key on the keyboard was pressed, and a whole lot more.

The only problem here is knowing when keyboard input is available, and that’s where epoll comes into the picture.

What we want is a system whereby we are notified when it is safe to read from the keyboard’s open file descriptor. Basically, whenever there is input available to be read. If we can get this notification, then we can then go ahead and read from the file descriptor. If we don’t receive this notification, then we know there’s nothing to read, and we can continue along with our business.

I’ve created an IOEventEmitter to deal with this situation. It looks like this:

IOEventEmitter = {}
IOEventEmitter_mt = {
	__index = IOEventEmitter,
}

function IOEventEmitter.new()
	local handle, err = S.epoll_create();
	if not handle then
		return false, err
	end

	local obj = {
		Handle	= handle,
	}

	setmetatable(obj, IOEventEmitter_mt);

	return obj;

end


function IOEventEmitter:Wait(timeout, events, maxevents)
	timeout = timeout or 0

	return S.epoll_wait(self.Handle, events, maxevents, timeout);
end

--[[
	event must have the following:
	Descriptor - file descriptor
	actions - bitwise OR of actions to observe
--]]

function IOEventEmitter:AddObserver(observer)
	local event = S.t.epoll_event();
	event.events = observer.actions;
	event.data.fd = observer.Descriptor:getfd();

	return S.epoll_ctl(self.Handle, S.c.EPOLL_CTL.ADD, event.data.fd, event);
end

function IOEventEmitter:RemoveObserver(observer)
	return S.epoll_ctl(self.Handle, S.c.EPOLL_CTL.DEL, observer.fd, nil); 
end

Using this is pretty straight forward.

local emitter = IOEventEmitter.new();
local keyObserver = AddKeyboardObserver(emitter, OnKey);

local timeout = 500
while true do
  local ret, err = emitter:Wait(timeout);

  if ret then
    for i=1,#ret do
      -- get the appropriate observer
      local observer = Observers[ret[i].fd];
      if observer and observer.Callback then
        observer.Callback(emitter, observer)
      end
    end
  end
  OnIdle();
end

To break it down, setup the emitter object as a first step. Then call this mysterious “AddKeyboardObserver()” function. This is nothing more than a simple convenience function that ties a ‘callback’ function to the file descriptor for the keyboard. It looks like this:

local Observers = {}
AddKeyboardObserver = function(emitter, onactivity, devicename)
	devicename = devicename or "/dev/input/event0"
	local fd, err = S.open(devicename, S.c.O.RDONLY);
	if not fd then
		return false, err
	end

	local observer = {
		Descriptor = fd, 
		actions = S.c.POLL.RDNORM,
		Callback = onactivity};

	Observers[fd:getfd()] = observer;

	emitter:AddObserver(observer)

	return observer;
end

Basically, open the keyboard file for read, and stick that file descriptor into a data structure of the form the emitter expects. The emitter itself only needs the file descriptor and the actions. The loop needs the callback.

This is the heart of darkness right here. The callback can be anything. Let’s say you want to do a different kind of loop. Let’s say that instead of calling a function right then and there, which is very IObservable, you wanted to simply place the ‘even’t onto a queue instead:

while true do
  local ret, err = emitter:Wait(timeout);

  if ret then
    for i=1,#ret do
      -- get the appropriate observer
      evenqueue:Enqueue(ret[i])
    end
  end
  eventqueue:Enqueue(idleevent);
end

This small change completely changes the behavior of your application. Suddenly doing things like being multi-threaded becomes much easier. You might have one, or more, emitters, sitting on different threads event, generating events, and placing them into queues, to be handled by some other parts of your program, which themselves might be in separate threads. For that matter, they might be sitting on different machines across a network.

Breaking things down to this level, I realize that there’s nothing too scary or special in the libev, libuv, libevent, or any other eventing library that you might want to use. At the very core, there is the event emitter, which can be implemented in myriad different ways (epoll being the current favorite). What you do with that emitter is completely up to you in user space. You can construct standard event loops, and taylor them for networking, or UI based applications, they can be IObservable, or IEnumerable. You don’t have to be constrained by what the underlying library provides for you. Depending on how you structure things, you can even have a mix of models, depending on the needs of your application.

With this revelation in hand, I’m going to go back and reconstruct some GUI applications, and some network servers and see where I get.

Advertisements


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s