项目作者: elijahr

项目描述 :
A lock-free ring buffer for Python and Cython
高级语言: Python
项目地址: git://github.com/elijahr/ringbuf.git
创建时间: 2020-03-11T05:28:19Z
项目社区:https://github.com/elijahr/ringbuf

开源协议:BSD 3-Clause "New" or "Revised" License

下载


ringbuf

A lock-free, single-producer, single-consumer, ring buffer for Python and Cython.

Test PyPI version Code style: black linting: pylint

Installation

OS X: brew install boost

Ubuntu: apt-get install libboost-all-dev

Windows: Install the latest version of Boost then set the BOOST_ROOT environment variable to point to its folder.

Then:

  1. pip install ringbuf

Motivation

When working with realtime DSP in Python, we might be wrapping some external C/C++ library (for instance, PortAudio) which runs some user-provided callback function in realtime. The callback function shouldn’t allocate/deallocate memory, shouldn’t contain any critical sections (mutexes), and so forth, to prevent priority inversion. If the callback were to contain Python objects, we’d likely be allocating and deallocating, and at the very least, acquiring and releasing the GIL. So, the callback cannot interact with Python objects if we expect realtime performance. As such, there’s a need for buffering data in a non-locking way between a C/C++ callback and Python.

Enter ringbuf, Cython wrappers for boost::lockfree::spsc_queue. Our Python code can read from and write to a ringbuf.RingBuffer object, and our C++ code can read from and write to that buffer’s underlying spsc_queue, no GIL required.

Usage

Any Python object which supports the buffer protocol can be stored in ringbuf.RingBuffer. This includes, but is not limited to: bytes, bytearray, array.array, and numpy.ndarray.

NumPy

  1. import numpy as np
  2. from ringbuf import RingBuffer
  3. buffer = RingBuffer(format='f', capacity=100)
  4. data = np.linspace(-1, 1, num=100, dtype='f')
  5. buffer.push(data)
  6. popped = buffer.pop(100)
  7. assert np.array_equal(data, popped)

bytes

  1. from ringbuf import RingBuffer
  2. buffer = RingBuffer(format='B', capacity=11)
  3. buffer.push(b'hello world')
  4. popped = buffer.pop(11)
  5. assert bytes(popped) == b'hello world'

Interfacing with C/C++

mymodule.pxd:

  1. # distutils: language = c++
  2. cdef void callback(void* q)

mymodule.pyx:

  1. # distutils: language = c++
  2. from array import array
  3. from ringbuf.boost cimport spsc_queue, void_ptr_to_spsc_queue_char_ptr
  4. from ringbuf.ringbufcy cimport RingBuffer
  5. from some_c_library cimport some_c_function
  6. cdef void callback(void* q):
  7. cdef:
  8. # Cast the void* back to an spsc_queue.
  9. # The underlying queue always holds chars.
  10. spsc_queue[char] *queue = void_ptr_to_spsc_queue_char_ptr(q)
  11. double[5] to_push = [1.0, 2.0, 3.0, 4.0, 5.0]
  12. # Since the queue holds chars, you'll have to cast and adjust size accordingly.
  13. queue.push(<char*>to_push, sizeof(double) * 5)
  14. def do_stuff():
  15. cdef:
  16. RingBuffer buffer = RingBuffer(format='d', capacity=100)
  17. void* queue = buffer.queue_void_ptr()
  18. # Pass our callback and a void pointer to the buffer's queue to some third party library.
  19. # Presumably, the C library schedules the callback and passes it the queue's void pointer.
  20. some_c_function(callback, queue)
  21. sleep(1)
  22. assert array.array('d', buffer.pop(5)) == array.array('d', range(1, 6))

Handling overflow & underflow

When RingBuffer.push() overflows, it returns the data that couldn’t be pushed (or None, if all was pushed):

  1. from ringbuf import RingBuffer
  2. buffer = RingBuffer(format='B', capacity=10)
  3. overflowed = buffer.push(b'spam eggs ham')
  4. assert overflowed == b'ham'

When RingBuffer.pop() underflows, it returns whatever data could be popped:

  1. from ringbuf import RingBuffer
  2. buffer = RingBuffer(format='B', capacity=13)
  3. buffer.push(b'spam eggs ham')
  4. popped = buffer.pop(buffer.capacity * 100)
  5. assert bytes(popped) == b'spam eggs ham'

For additional usage see the tests.

Supported platforms

GitHub Actions tests the following matrix:

  • Linux:
    • CPython 3.7
    • CPython 3.8
    • CPython 3.9
    • CPython 3.10
  • macOS:
    • CPython 3.10
  • Windows:
    • CPython 3.10

Any platform with a C++11 compiler and boost installed should work.

Contributing

Pull requests are welcome, please file any issues you encounter.

The code is linted with lintball. There is a pre-commit hook to lint, configured by running:

  1. npm install -g lintball
  2. git config --local core.hooksPath .githooks

Changelog

v2.6.0 2022-09-27

  • Move CI to GitHub Actions.
  • Lint codebase with lintball
  • Improve project structure

v2.5.0 2020-04-17

  • Added experimental support for Windows.

v2.4.0 2020-03-23

  • Added RingBuffer.reset() method to clear the buffer.

v2.3.0 2020-03-22

  • Added concatenate function for joining multiple arbitrary Python objects that support the buffer protocol.