Gruvi is an asynchronous I/O library for Python, just like asyncio, gevent and eventlet. It is therefore a fair to ask why I decided to create a new library. The short and simple answer is that I didn’t agree with some of the decision decision that went into these other projects, and thought it would be good to have a library based on different assumptions.
These are the design requirements that I had before creating Gruvi:
The table below compares some of the design decisions and features of Gruvi against asyncio, gevent and eventlet.
Note: tables like these necessarily compress a more complex reality into a much more limited set of answers, and they also a snapshot in time. Please assume good faith. If you spot an error in the table, please let me know and I will change it.
Feature | Gruvi | Asyncio | Gevent | Eventlet |
---|---|---|---|---|
IO library | libuv | stdlib | libev | stdlib / libevent |
IO abstraction | Transports / Protocols | Transports / Protocols | Green sockets | Green sockets |
Threading | fibers | yield from | greenlet | greenlet |
Resolver | threadpool | threadpool | threadpool / c-ares | blocking / dnspython |
Python: 2.x | YES (2.6+) | YES (2.6+, via Trollius) | YES | YES |
Python: 3.x | YES (3.3+) | YES | NO | NO |
Python: PyPy | NO | NO | YES | YES |
Platform: Linux | FAST | FAST | FAST | FAST |
Platform: Mac OSX | FAST | FAST | FAST | FAST |
Platform: Windows | FAST (proactor) | FAST (proactor) | SLOW (reactor) | SLOW (reactor) |
SSL: Posix | FAST | FAST | FAST | FAST |
SSL: Windows | FAST (proactor) | SLOW (reactor) | SLOW (reactor) | SLOW (reactor) |
SSL: Contexts | YES (also Py2.6+) | YES (also Py2.6+) | NO | NO |
HTTP | FAST (via http-parser) | NO (external) | SLOW (stdlib) | SLOW (stdlib) |
Monkey Patching | NO | NO | YES | YES |
Green threads have a very low memory overhead, and can therefore support a high level of concurrently. But more importantly, green threads are cooperatively scheduled. This means that a thread switch happens only when specifically instructed by a switch() call, and with a bit of care, we can write concurrent code that does not require locking.
When combining a thread implementation with an IO framework, one of the key design decisions is whether to implement explicit or implicit switching.
Green thread switching in Gruvi is implicit. This means that whenever a function would block, for example to wait for data from the network, it will switch to a central scheduler called the hub. The hub will then switch to another green thread if one is ready, or it will run the event loop. A function that can block is just like any other function. It is called as a regular function, and can call other (blocking and not) functions.
A common criticism of the implicit switching approach is that the locations where these switches happen, the so-called switch points, are not clearly marked. As a programmer you could call into a function that 3 levels down in the call stack, causes a thread switch. The drawback of this, according to the criticism, is that the switch points could happen essentially anywhere, and that therefore it’s like pre-emptive multi-threading where you need full and careful locking.
The alternative to implicit switching is explicit switching. This is the approach taken for example by the asyncio framework. In this approach, every switch point is made explicit, in the case of asyncio because it is not called as a normal function but instead used with the yield from construct.
In my view, a big disadvantages of the explicit approach is that the explicit behavior needs to percolate all the way up the call chain: any function that calls a switch point also needs to be called as a switch point. In my view this requires too much up-front planning, and it reduces composability.
Rather than implementing explicit switching, Gruvi sticks with implicit switching but tries to address the “unknown switch points” problem, in the following way:
In the end, the difference between implicit and explicit switching is a trade-off. In my view, with the safeguards of Gruvi e.g. the marking of switch points and the gruvi.assert_no_switchpoints context manager, the balance tips in favor of implicit switching. Some people have come to the same conclusion, others to a different one. Both approached are valid and as an programmer you should pick the approach you like most.
One other important design decision in Gruvi that I decided early on is not to implement monkey patching. Monkey patching is an approach employed by e.g. gevent and eventlet where they make the Python standard library cooperative by replacing blocking functions with cooperative functions using runtime patching.
In my experience, monkey patching is error prone and fragile. You end up distributing parts of the standard library yourself, bugs included. This is a maintenance burden that I’m not willing to take on. Also the approach is very susceptible to dependency loading order problems, and it only works for code that calls into the blocking functions via Python. Extension modules using e.g. the C-API don’t work, as well as extension modules that use an external library for IO (e.g. psycopg).
Finally, monkey patching does not work well with libuv because libuv provides a completion based interface while the standard library assumes a ready-based interface.
The solution that Gruvi offers is two-fold: