tutorial
Last updated
Was this helpful?
Last updated
Was this helpful?
[TOC]
The structure of this tutorial assumes an intermediate level knowledge of Python but not much else. No knowledge of concurrency is expected. The goal is to give you the tools you need to get going with gevent, help you tame your existing concurrency problems and start writing asynchronous applications today.
In chronological order of contribution:
Also thanks to Denis Bilenko for writing gevent and guidance in constructing this tutorial.
This is a collaborative document published under MIT license. Have something to add? See a typo? Fork and issue a pull request . Any and all contributions are welcome.
This page is also .
The primary pattern used in gevent is the Greenlet, a lightweight coroutine provided to Python as a C extension module. Greenlets all run inside of the OS process for the main program but are scheduled cooperatively.
Only one greenlet is ever running at any given time.
This differs from any of the real parallelism constructs provided by multiprocessing
or threading
libraries which do spin processes and POSIX threads which are scheduled by the operating system and are truly parallel.
The core idea of concurrency is that a larger task can be broken down into a collection of subtasks whose and scheduled to run simultaneously or asynchronously, instead of one at a time or synchronously. A switch between the two subtasks is known as a context switch.
A context switch in gevent is done through yielding. In this case example we have two contexts which yield to each other through invoking gevent.sleep(0)
.
[[[cog import gevent
def foo(): print('Running in foo') gevent.sleep(0) print('Explicit context switch to foo again')
def bar(): print('Explicit context to bar') gevent.sleep(0) print('Implicit context switch back to bar')
gevent.joinall([ gevent.spawn(foo), gevent.spawn(bar), ]) ]]] [[[end]]]
It is illuminating to visualize the control flow of the program or walk through it with a debugger to see the context switches as they occur.
The real power of gevent comes when we use it for network and IO bound functions which can be cooperatively scheduled. Gevent has taken care of all the details to ensure that your network libraries will implicitly yield their greenlet contexts whenever possible. I cannot stress enough what a powerful idiom this is. But maybe an example will illustrate.
In this case the select()
function is normally a blocking call that polls on various file descriptors.
[[[cog import time import gevent from gevent import select
start = time.time() tic = lambda: 'at %1.1f seconds' % (time.time() - start)
def gr1():
def gr2():
def gr3(): print("Hey lets do some stuff while the greenlets poll, at", tic()) gevent.sleep(1)
gevent.joinall([ gevent.spawn(gr1), gevent.spawn(gr2), gevent.spawn(gr3), ]) ]]] [[[end]]]
Another somewhat synthetic example defines a task
function which is non-deterministic (i.e. its output is not guaranteed to give the same result for the same inputs). In this case the side effect of running the function is that the task pauses its execution for a random number of seconds.
[[[cog import gevent import random
def task(pid): """ Some non-deterministic task """ gevent.sleep(random.randint(0,2)*0.001) print('Task', pid, 'done')
def synchronous(): for i in range(1,10): task(i)
def asynchronous(): threads = [gevent.spawn(task, i) for i in xrange(10)] gevent.joinall(threads)
print('Synchronous:') synchronous()
print('Asynchronous:') asynchronous() ]]] [[[end]]]
In the synchronous case all the tasks are run sequentially, which results in the main programming blocking ( i.e. pausing the execution of the main program ) while each task executes.
The important parts of the program are the gevent.spawn
which wraps up the given function inside of a Greenlet thread. The list of initialized greenlets are stored in the array threads
which is passed to the gevent.joinall
function which blocks the current program to run all the given greenlets. The execution will step forward only when all the greenlets terminate.
The important fact to notice is that the order of execution in the async case is essentially random and that the total execution time in the async case is much less than the sync case. In fact the maximum time for the synchronous case to complete is when each tasks pauses for 2 seconds resulting in a 20 seconds for the whole queue. In the async case the maximum runtime is roughly 2 seconds since none of the tasks block the execution of the others.
A more common use case, fetching data from a server asynchronously, the runtime of fetch()
will differ between requests given the load on the remote server.
As mentioned previously, greenlets are deterministic. Given the same configuration of greenlets and the same set of inputs and they always produce the same output. For example lets spread a task across a multiprocessing pool compared to a gevent pool.
Even though gevent is normally deterministic, sources of non-determinism can creep into your program when you begin to interact with outside services such as sockets and files. Thus even though green threads are a form of "deterministic concurrency", they still can experience some of the same problems that POSIX threads and processes experience.
The perennial problem involved with concurrency is known as a race condition. Simply put is when two concurrent threads / processes depend on some shared resource but also attempt to modify this value. This results in resources whose values become time-dependent on the execution order. This is a problem, and in general one should very much try to avoid race conditions since they result program behavior which is globally non-deterministic.
The best approach to this is to simply avoid all global state all times. Global state and import-time side effects will always come back to bite you!
gevent provides a few wrappers around Greenlet initialization. Some of the most common patterns are:
[[[cog import gevent from gevent import Greenlet
def foo(message, n): """ Each thread will be passed the message, and n arguments in its initialization. """ gevent.sleep(n) print(message)
thread1 = Greenlet.spawn(foo, "Hello", 1)
thread2 = gevent.spawn(foo, "I live!", 2)
thread3 = gevent.spawn(lambda x: (x+1), 2)
threads = [thread1, thread2, thread3]
gevent.joinall(threads) ]]] [[[end]]]
In addition to using the base Greenlet class, you may also subclass Greenlet class and override the _run
method.
[[[cog import gevent from gevent import Greenlet
class MyGreenlet(Greenlet):
g = MyGreenlet("Hi there!", 3) g.start() g.join() ]]] [[[end]]]
Like any other segment of code, Greenlets can fail in various ways. A greenlet may fail to throw an exception, fail to halt or consume too many system resources.</p>
The internal state of a greenlet is generally a time-dependent parameter. There are a number of flags on greenlets which let you monitor the state of the thread
started
-- Boolean, indicates whether the Greenlet has been started. </li>
ready()
-- Boolean, indicates whether the Greenlet has halted</li>
successful()
-- Boolean, indicates whether the Greenlet has halted and not thrown an exception</li>
value
-- arbitrary, the value returned by the Greenlet</li>
exception
-- exception, uncaught exception instance thrown inside the greenlet</li>
[[[cog import gevent
def win(): return 'You win!'
def fail(): raise Exception('You fail at failing.')
winner = gevent.spawn(win) loser = gevent.spawn(fail)
print(winner.started) # True print(loser.started) # True
try: gevent.joinall([winner, loser]) except Exception as e: print('This will never be reached')
print(winner.value) # 'You win!' print(loser.value) # None
print(winner.ready()) # True print(loser.ready()) # True
print(winner.successful()) # True print(loser.successful()) # False
print(loser.exception)
]]] [[[end]]]
Greenlets that fail to yield when the main program receives a SIGQUIT may hold the program's execution longer than expected. This results in so called "zombie processes" which need to be killed from outside of the Python interpreter.
A common pattern is to listen SIGQUIT events on the main program and to invoke gevent.shutdown
before exit.
Timeouts are a constraint on the runtime of a block of code or a Greenlet.
Or with a context manager in a with
statement.
In addition, gevent also provides timeout arguments for a variety of Greenlet and data stucture related calls. For example:
[[[cog import gevent from gevent import Timeout
def wait(): gevent.sleep(2)
timer = Timeout(1).start() thread1 = gevent.spawn(wait)
try: thread1.join(timeout=timer) except Timeout: print('Thread 1 timed out')
timer = Timeout.start_new(1) thread2 = gevent.spawn(wait)
try: thread2.get(timeout=timer) except Timeout: print('Thread 2 timed out')
try: gevent.with_timeout(1, wait) except Timeout: print('Thread 3 timed out')
]]] [[[end]]]
Events are a form of asynchronous communication between Greenlets.
A extension of the Event object is the AsyncResult which allows you to send a value along with the wakeup call. This is sometimes called a future or a deferred, since it holds a reference to a future value that can be set on an arbitrary time schedule.
Queues are ordered sets of data that have the usual put
/ get
operations but are written in a way such that they can be safely manipulated across Greenlets.
For example if one Greenlet grabs an item off of the queue, the same item will not grabbed by another Greenlet executing simultaneously.
[[[cog import gevent from gevent.queue import Queue
tasks = Queue()
def worker(n): while not tasks.empty(): task = tasks.get() print('Worker %s got task %s' % (n, task)) gevent.sleep(0)
def boss(): for i in xrange(1,25): tasks.put_nowait(i)
gevent.spawn(boss).join()
gevent.joinall([ gevent.spawn(worker, 'steve'), gevent.spawn(worker, 'john'), gevent.spawn(worker, 'nancy'), ]) ]]] [[[end]]]
Queues can also block on either put
or get
as the need arises.
Each of the put
and get
operations has a non-blocking counterpart, put_nowait
and get_nowait
which will not block, but instead raise either gevent.queue.Empty
or gevent.queue.Full
in the operation is not possible.
In this example we have the boss running simultaneously to the workers and have a restriction on the Queue that it can contain no more than three elements. This restriction means that the put
operation will block until there is space on the queue. Conversely the get
operation will block if there are no elements on the queue to fetch, it also takes a timeout argument to allow for the queue to exit with the exception gevent.queue.Empty
if no work can found within the time frame of the Timeout.
[[[cog import gevent from gevent.queue import Queue, Empty
tasks = Queue(maxsize=3)
def worker(n): try: while True: task = tasks.get(timeout=1) # decrements queue size by 1 print('Worker %s got task %s' % (n, task)) gevent.sleep(0) except Empty: print('Quitting time!')
def boss(): """ Boss will wait to hand out work until a individual worker is free since the maxsize of the task queue is 3. """
gevent.joinall([ gevent.spawn(boss), gevent.spawn(worker, 'steve'), gevent.spawn(worker, 'john'), gevent.spawn(worker, 'bob'), ]) ]]] [[[end]]]
A group is a collection of running greenlets which are managed and scheduled together as group. It also doubles as parallel dispatcher that mirrors the Python multiprocessing
library.
[[[cog import gevent from gevent.pool import Group
def talk(msg): for i in xrange(3): print(msg)
g1 = gevent.spawn(talk, 'bar') g2 = gevent.spawn(talk, 'foo') g3 = gevent.spawn(talk, 'fizz')
group = Group() group.add(g1) group.add(g2) group.join()
group.add(g3) group.join() ]]] [[[end]]]
This is very usefull for managing groups of asynchronous tasks that.
As mentioned above Group also provides an API for dispatching jobs to grouped greenlets and collecting their results in various ways.
[[[cog import gevent from gevent import getcurrent from gevent.pool import Group
group = Group()
def hello_from(n): print('Size of group', len(group)) print('Hello from Greenlet %s' % id(getcurrent()))
group.map(hello_from, xrange(3))
def intensive(n): gevent.sleep(3 - n) return 'task', n
print('Ordered')
ogroup = Group() for i in ogroup.imap(intensive, xrange(3)): print(i)
print('Unordered')
igroup = Group() for i in igroup.imap_unordered(intensive, xrange(3)): print(i)
]]] [[[end]]]
A pool is a structure designed for handling dynamic numbers of greenlets which need to be concurrency-limited. This is often desirable in cases where one wants to do many network or IO bound tasks in parallel.
[[[cog import gevent from gevent import getcurrent from gevent.pool import Pool
pool = Pool(2)
def hello_from(n): print('Size of pool', len(pool))
pool.map(hello_from, xrange(3)) ]]] [[[end]]]
Often when building gevent driven services one will center the entire service around a pool structure. An example might be a class which polls on various sockets.
A semaphore is a low level synchronization primitive that allows greenlets to coordinate and limit concurrent access or execution. A semaphore exposes two methods, acquire
and release
The difference between the number of times and a semaphore has been acquired and released is called the bound of the semaphore. If a semaphore bound reaches 0 it will block until another greenlet releases its acquisition.
[[[cog from gevent import sleep from gevent.pool import Pool from gevent.coros import BoundedSemaphore
sem = BoundedSemaphore(2)
def worker1(n): sem.acquire() print('Worker %i acquired semaphore' % n) sleep(0) sem.release() print('Worker %i released semaphore' % n)
def worker2(n): with sem: print('Worker %i acquired semaphore' % n) sleep(0) print('Worker %i released semaphore' % n)
pool = Pool() pool.map(worker1, xrange(0,2)) pool.map(worker2, xrange(3,6)) ]]] [[[end]]]
A semaphore with bound of 1 is known as a Lock. it provides exclusive execution to one greenlet. They are often used to ensure that resources are only in use at one time in the context of a program.
The actor model is a higher level concurrency model popularized by the language Erlang. In short the main idea is that you have a collection of independent Actors which have an inbox from which they receive messages from other Actors. The main loop inside the Actor iterates through its messages and takes action according to its desired behavior.
Gevent does not have a primitive Actor type, but we can define one very simply using a Queue inside of a subclassed Greenlet.
In a use case:
[[[cog
pip install pyzmq gevent_zeromq
import gevent from gevent_zeromq import zmq
context = zmq.Context()
def server(): server_socket = context.socket(zmq.REQ) server_socket.bind("tcp://127.0.0.1:5000")
def client(): client_socket = context.socket(zmq.REP) client_socket.connect("tcp://127.0.0.1:5000")
publisher = gevent.spawn(server) client = gevent.spawn(client)
gevent.joinall([publisher, client])
]]] [[[end]]]
Gevent provides two WSGI servers for serving content over HTTP. Henceforth called wsgi
and pywsgi
:
gevent.wsgi.WSGIServer
gevent.pywsgi.WSGIServer
In earlier versions of gevent before 1.0.x, gevent used libevent instead of libev. Libevent included a fast HTTP server which was used by gevent's wsgi
server.
In gevent 1.0.x there is no http server included. Instead gevent.wsgi
is now an alias for the pure Python server in gevent.pywsgi
.
If you are using gevent 1.0.x, this section does not apply
For those familiar with streaming HTTP services, the core idea is that in the headers we do not specify a length of the content. We instead hold the connection open and flush chunks down the pipe, prefixing each with a hex digit indicating the length of the chunk. The stream is closed when a size zero chunk is sent.
The above HTTP connection could not be created in wsgi because streaming is not supported. It would instead have to buffered.
Using pywsgi we can however write our handler as a generator and yield the result chunk by chunk.
But regardless, performance on Gevent servers is phenomenal compared to other Python servers. libev is a very vetted technology and its derivative servers are known to perform well at scale.
HTML Page:
Minimal websocket application $(function() { // Open up a connection to our server var ws = new WebSocket("ws://localhost:10000/"); // What do we do when we get a message? ws.onmessage = function(evt) { $("#placeholder").append('<p>' + evt.data + '</p>') } // Just update our conn_status field with the connection status ws.onopen = function(evt) { $('#conn_status').html('<b>Connected</b>'); } ws.onerror = function(evt) { $('#conn_status').html('<b>Error</b>'); } ws.onclose = function(evt) { $('#conn_status').html('<b>Closed</b>'); } });
Not Connected
is described by its authors as "a socket library that acts as a concurrency framework". It is a very powerful messaging layer for building concurrent and distributed applications.
ZeroMQ provides a variety of socket primitives, the simplest of which being a Request-Response socket pair. A socket has two methods of interest send
and recv
, both of which are normally blocking operations. But this is remedied by a briliant library by which uses gevent.socket to poll ZeroMQ sockets in a non-blocking manner. You can install gevent-zeromq from PyPi via: pip install gevent-zeromq
To benchmark, try Apache Benchmark ab
or see this for comparison with other servers.
Websocket example which requires .
The final motivating example, a realtime chat room. This example requires ( but not neccesarily so, you could use Django, Pyramid, etc ). The corresponding Javascript and HTML files can be found .