Curio is a modern library for performing reliable concurrent I/O using Python coroutines and the explicit async/await syntax introduced in Python 3.5. Its programming model is based on cooperative multitasking and common system programming abstractions such as threads, sockets, files, subprocesses, locks, and queues. Under the covers, it is based on a task queuing system that is small, fast, and powerful.
Here is a simple TCP echo server implemented using sockets and curio:
# echoserv.py from curio import Kernel, new_task from curio.socket import * async def echo_server(address): sock = socket(AF_INET, SOCK_STREAM) sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) sock.bind(address) sock.listen(5) print('Server listening at', address) async with sock: while True: client, addr = await sock.accept() await new_task(echo_client(client, addr)) async def echo_client(client, addr): print('Connection from', addr) async with client: while True: data = await client.recv(1000) if not data: break await client.sendall(data) print('Connection closed') if __name__ == '__main__': kernel = Kernel() kernel.run(echo_server(('',25000)))
If you have programmed with threads, you find that curio looks similar. You'll also find that the above server can handle thousands of simultaneous client connections even though no threads are being used under the covers.
Of course, if you prefer something a little higher level, you can have curio take of the fiddly bits related to setting up the server portion of the code:
# echoserv.py from curio import Kernel, new_task, run_server async def echo_client(client, addr): print('Connection from', addr) while True: data = await client.recv(1000) if not data: break await client.sendall(data) print('Connection closed') if __name__ == '__main__': kernel = Kernel() kernel.run(run_server('', 25000, echo_client))
This is only a small sample of what's possible. Read the official documentation for more in-depth coverage. The tutorial is a good starting point.
Curio provides additional support for SSL connections, synchronization primitives (events, locks, semaphores, and condition variables), queues, Unix signals, subprocesses, as well as thread and process pools. In addition, the task model fully supports cancellation, timeouts, monitoring, and other features critical to writing reliable code.
Python already has a variety of libraries for async and event driven I/O. So, why create yet another library? There is no simple answer to that question, but here are a few of the motivations for creating curio.
- Python 3 has evolved considerably as a programming language and has adopted many new language features that are well-suited to cleanly writing a new I/O library. For example, improved support for non-blocking I/O, support for delegation to subgenerators (yield from) and the introduction of explicit async and await syntax in Python 3.5. Curio takes full advantage of these features and is not encumbered by issues of backwards compatibility with legacy Python code written 15 years ago.
- Existing I/O libraries are mostly built on event-loops, callback functions, and custom I/O abstractions--this includes Python's own asyncio module. Curio takes a completely different approach to the problem that focuses almost entirely on task scheduling while relying upon known I/O techniques involving sockets and files. If you have previously written synchronous code using processes or threads, curio will feel familiar.
- Curio is a powerful library in a small package. An emphasis is placed on implementation simplicity. Simplicity is an important part of writing reliable systems software. When your code fails, it helps to be able to debug it--possibly down to the level of individual calls to the operating system if necessary. Simplicity matters a lot. Simple code also tends to run faster.
- It's fun.
Internally, curio is implemented entirely as a task queuing system-- much in the same model as how an actual operating system kernel works. Tasks are represented by coroutine functions declared with the async keyword. Each yield of a coroutine results in a low-level kernel "trap" or system call. The kernel handles each trap by moving the current task to an appropriate waiting queue. Events (i.e., due to I/O) and other operations make the tasks move from waiting queues back into service.
It's important to emphasize that the kernel is solely focused on task management, scheduling, and nothing else. In fact, the kernel doesn't even perform any I/O operations. This means that it is very small, fast, and straightforward to understand.
Higher-level I/O operations are carried out by a wrapper layer that
uses Python's normal socket and file objects. You use the
same operations that you would normally use in synchronous code except
that you add await
keywords to methods that might block.
Q: Is curio implemented using the asyncio module?
A: No. Curio is a standalone library. Although the core of the library
uses the same basic machinery as asyncio
to poll for I/O events,
the handling of those events is done in a completely different manner.
Q: Is curio meant to be a clone of asyncio?
A: No. Although curio provides a significant amount of overlapping functionality, some of the APIs are slightly different. Compatibility with other libraries is not a goal.
Q: How many tasks can be created?
A: Each task involves an instance of a Task
class that
encapsulates a generator. No threads are used. As such, you're really
only limited by the memory of your machine--potentially you could have
hundreds of thousands of tasks. The I/O functionality in curio is
implemented using the built-in selectors
module. Thus, the number
of open sockets allowed would be subject to the limits of that library
combined with any per-user limits imposed by the operating system.
Q: Can curio interoperate with other event loops?
A: At this time, no. However, curio is a young project. It's something that might be added later.
Q: How fast is curio?
A: In preliminary benchmarking of a simple echo server, curio runs
about 50-70% faster than asyncio
. It runs about 30-40% faster
than Twisted and about 10-15% slower than gevent, both running on
Python 2.7. This is on OS-X so your mileage might vary. See the
examples/benchmark
directory of the distribution for this testing
code.
Q: Is curio going to evolve into a framework?
A: No. The current goal is merely to provide a small, simple library for performing concurrent I/O. It is not anticipated that curio would evolve into a framework for implementing application level protocols such as HTTP. Instead, it might serve as a foundation for other packages that want to provide that kind of functionality.
Q: What are future plans?
A: Future work on curio will primarily focus on features related to debugging, diagnostics, and reliability. A primary goal is to provide a solid environment for running and controlling concurrent tasks.
Q: How big is curio?
A: The complete library currently consists of fewer than 1500 lines of source statements. This does not include blank lines and comments.
Q: Can I contribute?
A: Absolutely. Please use the Github page at https://github.com/dabeaz/curio as the primary point of discussion concerning pull requests, bugs, and feature requests.
Curio was created by David Beazley (@dabeaz). http://www.dabeaz.com
It is a young project. Contributions welcome.