Released: greenlight 0.1, helping write code with gevent

gevent may be much faster than Twisted for asynchronous IO, but (also like Twisted) the code can get a little ugly, due to the boilerplate needed to chain greenlets and callbacks. One of the nicer helpers in Twisted is inlineCallbacks, which lets you, for the most part, write asynchronous code in order, as if it were synchronous. Among other things, it's essential for readable unit tests. When I started using gevent in a project, I missed the pattern. Thus, greenlight!

Similar to inlineCallbacks, greenlight lets you flatten your greenlet-using code into something approaching readability, by yielding greenlets from a generator. It will chain them for you and execute them in order, so you can write your code like synchronous code while still having it run in the event loop.

Almost as a side effect, it's also useful as a decorator for simple functions that will cause them to be run in a greenlet.

Code is here, download here, or install via pip greenlight.

7 comments

  • Denis  
    October 20, 2010 12:56 AM

    It's an interesting exercise, but I don't think it solves any actual problem. Not only that but it complicates the code.

    I've translated the examples of greenlight into not using greenlight here: http://code.google.com/p/gevent/wiki/Greenlight

    Don't you think that pure gevent versions have less boilerplate?

    BTW, Twisted and gevent's main difference is not performance (in fact, it wasn't an explicit goal to be faster than Twisted; it's accidental of using a C library underneath). Twisted and gevent's difference is the API: you almost never need to use callbacks with gevent and there's no boilerplate. Twisted's inlineCallbacks goes towards making async code looking synchronous, but it cannot come close to gevent (unless it employs greenlet or Stackless Python).

    Cheers,
    Denis.

  • Ian McCracken  
    October 20, 2010 1:12 AM

    Well, admittedly, those are cruddy examples, merely meant to illustrate the idea. They'd be similarly stupid in Twisted.

    The scenario I'm thinking of is a tight while loop (for example), where you want to give time to the event loop, but maybe don't necessarily want to call gevent.spawn for each step. You still have to break it up into separate functions, of course, but I think the code ends up being more readable if you're actually just calling f(); g(); h() rather than calling gevent.spawn(f); gevent.spawn(g); gevent.spawn(h).

    I indeed recognize that error handling works as expected with gevent; that was just to prove that it wasn't altered by greenlight.

    Thanks for the comment. I've only used gevent in two projects so far, but I agree, the API is cleaner than that of Twisted (though I'm using it for the performance).

  • Denis  
    October 20, 2010 1:29 AM

    Maybe you could give a realistic example of what do you mean by "tight loop"?

  • Ian McCracken  
    October 20, 2010 1:42 AM

    Sure. In one case, I had a process that had to do several things: receive notifications of object changes that required some action, perform those actions, respond to requests from various sources. The problem occurred due to the object changes; when there were a lot, it took a long time, blocking:

    for change in changes:
    handleChange(change)

    That's paraphrased, of course. handleChange() itself did a lot of stuff. Anyway, in certain cases, thousands of changes would come in every second for several seconds. This took a while to deal with, and the rest of the process stalled and built up queued tasks, because the for block never gave the event loop a chance.

    Solved this by breaking up handleChange() into several independent functions, each of which had to execute in order but could be scheduled individually, and also scheduling each call to handleChange() independently (but in order; order was important). Thus it proceeded synchronously but deferred to the event loop so other things could get time.

    So that's the case I needed to make a little easier. I mean, it isn't hard or anything; you just have to call spawn() a lot, and the code ends up looking like:

    for change in changes:
    g = spawn(a)
    g2 = g.link(spawn(b))
    g3 = g.link(spawn(c))

  • Denis  
    October 20, 2010 2:25 AM

    Gevent way of doing that would be:

    for change in changes:
    a()
    gevent.sleep(0)
    b()
    gevent.sleep(0)
    c()

    Instead of 0 you can use 0.001, depending on what gives the best results in practice.

    Note, that if a, b or c already use cooperative API (anything in gevent package that blocks) then sleep is unnecessary.

    Also note, that if your gevent applications does a lot of CPU work it might make sense to offload that work to a process pool or thread pool.

  • Ian McCracken  
    October 20, 2010 2:31 AM

    Right, and it's the calls to sleep(0), or lots of spawns, that I want to avoid, but I do see your point.

    In this particular case threading or multiprocessing was not an option, but sure, yeah, ideally.

  • Alex  
    May 23, 2012 9:13 AM

    Hi Ian,

    I would like to know more about problem greenlight solves. Can you post some more details ?

    So far I understood that code:

    for change in changes:
    handleChange(change)

    can starve gevent hub when there are many elements in `changes` list. Can that be solved with cooperative yield in right place like so:

    for change in changes:
    gevent.sleep(0)
    handleChange(change)

Post a Comment