Tuesday, June 19, 2012

An Event Loop Architecture in C# similar to Node.js

I Really Like Node.js


As some of you know I've been working with Node.js quite a bit recently. There are a lot of things I like about it, and a few things I don't. I'm not going to get into all of that now. I think it's a great tool, and I'll continue to use it. But thinking about it's shortcomings had me wondering if an architecture like that was possible in my "home language" of C#.

So while I was showering, or pooping, or some bathroom related event (all of my best thoughts are usually in a bathroom I think), I came up with an idea of how I could implement an Event Loop in C#. The idea is simple enough, start a thread (or maybe more than one if I want) that pulls Actions off of a FIFO Queue and executes them. The second step would then be to implement I/O in a non-blocking way.

Why An Event Loop?


Well, simply put, it's non-blocking. Which means threads are used to their fullest potential. With other, synchronous architectures, threads chug along doing their thing until they need to wait for I/O (reading something from a disk, waiting for something over the network, waiting for user input, etc). At that point they block, which means they're sitting their doing nothing. With an Event Loop architecture, the thread is plugging away at bits of code (events) that have queued up. When one of those nasty I/O steps needs to be done, the architecture sends that off to some other thread in a thread pool, then continues processing what's next in the queue. The end result is that the main processing thread does not block and uses its thread to it's fullest potential.

I realize there are much, much, better explanations of the Event Loop architecture out there, and I've probably butchered the explanation to some degree, but I think that gets the general idea across.

Introducing ALE


So I created a new project, which I've open sourced on github called ALE (Another Looping Event)... I'm not sure it's the best name, and I'm up for suggestions, but ALE... beer... beer is good. Seemed good enough. Especially for what started off as a simple toy project or proof of concept.

This project thus far represents a brainstorm. A lot of fun effort, but it's an experiment at this stage, very alpha. What I'm really hoping for is your feedback. If I'm doing something wrong, tell me. If I could do something better, tell me. If you'd like to contribute, let me know. This has been a fun project to play with and I'm pretty excited about it.


Here is a basic example of how you can implement a simple web server:

UPDATE: I've added an asynchronous http handler so IIS can handle the incoming requests and send them into the event loop. More about that here.

using ALE.Http;

//Start the EventLoop and start the web server.
EventLoop.Start(() => {
   Server.Create((req, res) => {
      res.Write("Hello World");
   }).Listen("http://*:1337/");
});


Here's an example of some file system interaction:

using ALE.FileSystem;

//Start the EventLoop so it can process events.
EventLoop.Start(() => {
   //Read all of the text from a file, asynchronously.
   File.ReadAllText(@"C:\Foo.txt", (text) => {
      Console.WriteLine(text);
   });
});


An example of a simple web socket server.

using ALE.Tcp;

EventLoop.Start(() => {
   //Create a new web socket server.
   Net.CreateServer((socket) => {
      //send data to the client.
      socket.Send("Wee! sockets!");
      //set up callbacks for receiving data.
      socket.Recieve((text) => {
          //do something here.
      });
   }).Listen("127.0.0.1",1337,"http://origin/");
});

I've gone through the trouble of adding a few other things, a basic SqlClient implementation, for example. In the near future I'm planning on implementing some Entity Framework integration, Web Server middleware (ala Node.js's Express modules), web request routing, and Web Sockets.

But first thing is first, I'm pleading my much smarter developer friends to tell me what I'm doing wrong. I realize it's light on documentation (there is really none, haha)... but have a look and play around. It's pretty simplistic.

---
EDIT: Comments and recommendations below are dead on and I did a little research and wrote a bit about my findings here.

---
EDIT 2: Updated the syntax to reflect current project state.

---
EDIT 3: Updated the examples to include a web socket server.

14 comments:

  1. In your filesystem methods, you are basically scheduling blocking calls on the event loop.

    It would be much better to make use of the existing infrastructure in .NET to do asynchronous I/O. This will result in the actual I/O work being done outside of the event loop, and then when the work completes you can schedule just the *continuation* (which does something with that result) on the event loop.

    Before .NET4.5, you can use methods like Stream.BeginRead for that.

    .NET4.5 introduces new methods like Stream.ReadAsync which return a task , to play well with the new "await" feature in C#.

    ReplyDelete
  2. Ah nevermind, I focused on your use of synchronous I/O methods and missed the fact that you execute them via Action.BeginInvoke.

    Though I still think that it might be more efficient to use the existing methods for async I/O.

    ReplyDelete
  3. @Wim: Thanks for the feedback. Is there a method in particular? I'm almost positive that most or all of those calls were made inside of an Action called with BeginInvoke, so they should be run asynchronously in another thread. The general format I've done most of those modules in was:

    new Action() { /* do something here */ EventLoop.Current.Pend(callback); }.BeginInvoke(null, null);

    From all of my (quick and sloppy) testing, that isn't blocking. But I'll have another look.

    ReplyDelete
  4. That's a really good suggestion, Wim, I was thinking about that this morning and I'll definitely look into doing things that way.

    Another thing that I was looking into is the asynchronous methods available on the HttpListener like BeginGetContext. For now, I'm just using the single GetContext, but I think I could accomplish more simultaneous connections with the former.

    ReplyDelete
  5. I think this way doesn't make much sense for a webserver. ASP.NET already uses ThreadPool for handling requests. And using BeginInvoke() to call synchronous methods means it won't block the current thread, yes. But it will just block different thread instead.

    Before reinventing the wheel, you should look how you can already do asynchronous IO in .Net (using methods like BeginWrite()), and especially into async-await coming in C# 5.

    ReplyDelete
  6. @svick: a "non-blocking" model doesn't mean "no thread ever blocks". I/O of any sort will block a thread, no matter whether you're calling .Net's async methods, or rolling your own with BeginInvoke. The idea behind the event loop is to keep your worker thread(s) as busy as possible. ASP.Net on IIS does use the thread pool, but doesn't ensure one thread can handle the functional body of multiple requests like an event loop model does.

    ReplyDelete
  7. @Anonymous No, asynchronous IO in Windows really doesn't block any threads.

    And if you use asynchronous methods in ASP.NET correctly (with or without C# 5 await), that's exactly what will happen: the worker threads will be kept as busy as possible. On the other hand, if you use event loop with BeginInvoke(), that won't happen: many threads could be blocked at the same time, which will decrease efficiency.

    ReplyDelete
  8. @svick is right. Add was suggested above, I need to be leveraging .Net's Async I/O methods in order to take advantage of IO Completion Ports that prevent thread use during I/O operations. I've been looking at this since Wim said something yesterday. I'm going to update the project and also see if I can figure out a good way to test this in an automated way. (the might prove to be too difficult, we'll see)

    As for just not trying to make something like this because I'm "reinventing the wheel", well it's sort of the point of this to learn more than anything else. I'm not doing this for production purposes. So I think that's bad advice to give anyone.

    ReplyDelete
  9. @Benjamin I'm sorry. If you're doing this just to learn, then yeah, “do not reinvent the wheel” is a bad advice. I guess I didn't catch that from your article.

    But I think it's a good idea to first learn about the existing wheels, especially the standard ones. (And shiny new wheels, that are currently a Release Candidate.)

    ReplyDelete
  10. No sweat. It's just in the title of the blog (well subtitle) :P...

    Anyhow, I checked in some changes that follow advice given by you and Wim. I also changed the usage around a little (an example can be seen in the Program.cs of the test console project).

    This has been really fun to work on, and I'm really glad a friend of a friend posted my blog entry on Reddit so I could get feedback like this. Learning new things makes this profession worthwhile.

    ReplyDelete
  11. I think there is a way for calling an IO operation async. This depends on the operating system. in windows it is called I/O Completion Ports [http://msdn.microsoft.com/en-us/library/windows/desktop/aa365198%28v=vs.85%29.aspx]
    this way the OS is responcible for carrying on the IO operation, and returns an event when finished. (that is what Node.js uses)

    ReplyDelete
  12. @DB: you're 100% correct. I fact I talked a little bit more about that in another blog entry here: http://www.benlesh.com/2012/06/attempting-to-peer-up-skirt-of-non.html

    ReplyDelete
  13. Hi,
    I wanted to understand how you designed the infinite loop using continuous use of ContinueWith() in a chain, so I tried running the delegate snippet such as the following, passing them to the EventLoop.Start() sequentially 3 times.

    ()=>{
    for(;;){
    var line = Console.ReadLine();
    }
    }

    Occasionally, the last one passed to the EventLoop.Start() does not start and I assume the reason for this is that the following code reaches the Console.ReadLine() in the second delegate before the ()=>{} becomes the predecessor of the third delegate passed to the function.

    incomingTask.ContinueWith(w2 => evt());

    Is this meant to be this way?

    ReplyDelete
    Replies
    1. Will, since Console.ReadLine() is I/O, that should probably be handled asynchronously to fit with the architecture. I'm not sure I've added that... But I'd like to see your demo so I can get a better idea of what you mean.

      Delete

This form allows some basic HTML. It will only create links if you wrap the URL in an anchor tag (Sorry, it's the Blogger default)