Skip to content
Bytes by Ying
Go back

Concurrency with Python: CSP and Coroutines

Edit page

The Concurrency with Python Series:


Overview

The concept of communicating sequential processes, or CSP, is similar to the notion of actor models, but brings added utility to contemporary concurrency challenges. Both of these concurrency models leverage message passing, but whereas actors pass messages between containers of state, the CSP model passes messages between channels, a form of synchronization and communication between coroutines or coroutine-like processes.

Russ Cox has a great writeup about the history of CSP.

This mode of concurrency addresses a number of difficulties with implementing concurrency in production:

You may not want to use CSP if:

The most popular example of CSP in a programming language at the moment is golang, whose concurrency model is designed around a derivative of Hoare’s CSP process calculus and implements channels as first-class citizens.

You can see the CSP scheduler implemented in golang/src/runtime/proc.go.

Paul Butcher discusses CSP in the context of Clojure’s core.async, which is CSP as a library. Rich Hickey describes core.async in this Clojure release article.

Python CSP Libraries

Python does not natively support CSP or channels as first-class citizens. However, a number of small academic projects have provided a base layer from which a CSP framework could arise:

Python CSP Primitives

Unlike other concurrency models, which suffer from a lack of Pythonic foundations, implementing a CSP process calculus could be done pythonically through the use of Python’s native and various coroutines and coroutine libraries.

Python’s async Library

Python’s development of coroutines began with realizing how generator expressions, combined with the yield from and .send() keywords, results in the same inversion of control that allows for separate tasks to be concurrently scheduled. Abu Ashraf Masnun wrote a great blog post on this evolutionary process.

Truly native coroutines, separate from generator coroutines, with specifically targeted async/await syntax, asynchronous context management, and standard library support in inspect and sys (among other features), came with implementation of PEP 492 in Python 3.5.x.

Many libraries are racing to take advantage of this native support, a demonstration of how acceptance of a model of programming in the Python standard library drastically and effectively increases user adoption.

Stackless Python, greenlet, and gevent

Stackless Python is a fork of CPython that supports additional concurrency primitives. The ones relevant to the discusson on CSP include tasklets and channels. Both of these map well to CSP’s ideas of coroutines and channels.

greenlet is an evolution of Stackless Python’s idea of tasklets, except that scheduling and other control flow primitives are handled within user code. Tasklets are a form of microthread, whereas greenlets are a form of coroutine. Guido discusses the difference between the two in a mailing list archive:

Microthreads are “almost real” threads, with round-robin scheduling. What makes them attractive to some is the fact that they don’t use operating system resources: there’s no OS-level stack or kernel process table entry associated with them. This also accounts for their biggest weakness: when a microthread is suspended for I/O, all microthreads are suspended. In limited contexts, such as a network server, this can be solved by an approach similar to that in Gordon’s SelectDispatcher. (It would be a titanic effort to do this for every potentially blocking I/O operation, and it probably wouldn’t work well with C extensions.)

gevent combines the greenlet library with the libev event loop library, and provides Python bindings. Some interesting Python libraries, such as locust (an HTTP load testing framework), and gunicorn (a web server framework), are built on top of gevent.

Experimental / In-Progress CSP Initiatives

A handful of experimental, Python-specific concurrency initiatives are in the works. If they make it to a stable release point, they may one day form the foundations for CSP in Python (among other types of concurrency models).

Sub-interpreters

Eric Snow opened PEP 525 describing an initiative to utilize subinterpreters within a main Python interpreter, where the global scope of all of Python, including the C extensions API, would be moved one level down in the Python process space. This may help Python natively support an orchestration layer within the language, instead of using a third-party orchestration tool to coordinate multiple distinct Python services. Eric explicitly mentions CSP as an inspiration for how subinterpreters may communicate with one another:

Concurrency is a challenging area of software development. Decades of research and practice have led to a wide variety of concurrency models, each with different goals. Most center on correctness and usability.

One class of concurrency models focuses on isolated threads of execution that interoperate through some message passing scheme. A notable example is Communicating Sequential Processes (CSP) (upon which Go’s concurrency is roughly based). The isolation inherent to subinterpreters makes them well-suited to this approach.

pypy

pypy is an evolution of Stackless Python, that combines pluggable garbage collectors, a just-in-time compiler, and compatibility with most existing CPython code, along with greenlets, in order to make native Python more performant. pypy implements greenlets on top of a “continulet”, which is further implemented on top of a “stacklet”. As far as I can tell, this effort is to make context switching composable and deterministic.

A Concurrent Sieve of Erathostenes with pycsp

The Sieve of Erathostenes is a popular computer science problem that calculates the prime numbers in a bounded sequence by labeling all multiples of an existing prime.

golang implements a neat algorithm to compute sequential primes using channels; this example is runnable within the golang playground. The general gist of the algorithm is to have one channel create a stream of sequentially increasing values, and daisy chain other channels to the stream to drop values that are multiples. The outputs of the daisy chain are the prime numbers in the sequence. However, as Stian Eikeland notes in his blog post on the same topic using Clojure’s core.async, and as this Hacker News discussion posits, this concurrency demonstration is mostly academic, as it is not very performant due to every prime being touched by every channel before the maximum of its prime factorization is reached.

pycsp has a version of this sieve implementation hosted on its website, but testing the provided algorithm on a development version of pycsp checked out at HEAD/fb9e32fd8aa88a33acce40d31aafcfe6693f0fff fails to run. After manually patching socket handling such that type bytes was explicitly set:

# pycsp/pycsp/parallel/ossocket.py:30
def _get_interface_ip(ifname):
    s = socket.socket(
        socket.AF_INET,
        socket.SOCK_DGRAM
    )
    ip = socket.inet_ntoa(fcntl.ioctl(
            s.fileno(),
            0x8915,  # SIOCGIFADDR
            # NOTE: Line below was originally
            # `struct.pack(
            #    '256s',
            #    ifname[:15]
            # )`.
            struct.pack(
                '256s',
                bytes(ifname[:15], 'utf-8')
            )
        )[20:24])
    s.close()
    return ip

The example, as posted here, successfully logs primes between 2 and 2000 to stdout.

Conclusion

Python has non-trivial limitations when it comes to natively implementing CSP. This golang Google Groups discussion is rather eye-opening in terms of exposing how differently golang and Python prioritize inter-process communication in syntax and typing. At the same time, the flexible nature of CSP lends itself to easier implementation on a language that does not natively support it, as compared to other concurrency models. While CSP in Python is still an academic discussion, a stable release of Python sub-interpreters or a production-ready CSP on async Python library may make discussions about CSP-like concurrency in production Python environments worthwhile.


Edit page
Share this post on:

Previous Post
Concurrency with Python: Hardware-Based Parallelism
Next Post
Concurrency with Python: Actor Models