Question

I wrote a small module which accepts MIDI input from a MIDI device using the RtMidi library: http://www.music.mcgill.ca/~gary/rtmidi/

I have no issues using the library, it works great. I've exported my module for use with Python using Cython. The module imports fine and works correctly with Python. The problem is that since this waits for user MIDI input indefinitely, it is necessary to spawn this in its own thread.

I've tested spawning this in its own thread in C++ using std::thread, and it works just fine. The problem is specifically trying to spawn this on its own thread from within Python when using the exported library.

Here's the code:

    import midi   #my exported C++ module
    import threading
    import time

    def test():
      for i in range(0, 10):
        print(i)
        time.sleep(.25)

    my_thread = threading.Thread(target=test)
    my_thread.daemon = False
    my_thread.start()

    midi_input = midi.MidiListen()
    midi_input.start()   #listen for MIDI input indefinitely

What winds up happening is that the thread I've spawned will print '0', and then the MIDI input reader will start, and the first thread will not continue printing until the midi_input.start() function returns, instead of both functions running concurrently.

Additionally, I've tried running this code the other way around where the midi_input.start() is executed on a separate thread instead of the test() function.

What am I doing wrong here?

EDIT: I was able to fix my problem thanks to Geoff's answer.

Notice the "nogil" and "with gil" at the end of the function declarations.

Here's my .pyx file:

from libcpp.vector cimport vector

cdef extern from "hello_midi.h":
  cdef cppclass MidiInput:
      void mycallback(double deltatime, vector[unsigned char]* message, void* userData) with gil
      int midi_listen() nogil

cdef void midi_init(MidiInput* ob):
  with nogil:
      ob.midi_listen()

cdef class MidiListen:
cdef MidiInput* thisptr
def __cinit__(self):
    self.thisptr = new MidiInput()
def start_midi(self): 
    midi_init(self.thisptr)

And here's the final Python script:

import midi
import threading
import time


def test():
    for i in range(0, 10):
        print(i)
        time.sleep(.25)

def start():
    x = midi.MidiListen()
    x.start_midi()

thread = threading.Thread(target=start)
thread.start()

test()
Was it helpful?

Solution

This is because of Python's global interpreter lock (GIL). Only one thread is allowed to run the in the Python interpreter at a time. To fix your problem you should release the GIL before calling into RtMidi. The Cython manual discusses this in the section Acquiring and Releasing the GIL.

Your new code will look something like

def start(self):
    with nogil:
        # Call rtmidi functions

However you must acquire the GIL if you are going to manipulate any Python objects, e.g. in a callback. It looks like RtMidi is callback oriented so if you currently have

cdef void callback(...):
    # Code that posts the midi event to python somehow

you should change it to

cdef void callback(...) with gil:
    # The same code as before

This causes Cython to emit code that gets the GIL lock back before running the function's body.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top