Question

I am writing a small Python script that makes heavy use of threads / multiple processes and I came across an interesting race condition that I cannot find an explanation for. I have a background process that plays a song that the user can skip or that can end on it's own, in both these cases I need to fire a callback to say it has ended.

Below is a psuedo code sample demonstrating the problem :

import threading, subprocess

def play_song(song_name, callback) :
    stop_any_previous_songs()
    play_song_in_subprocess_then_call(callback)

def main(self) :
    while True :
        music_stopped = threading.Event()
        def play_callback() :
            # say that the song stopped
            music_stopped.set()

        #Assume that the thread is actually a daemon
        threading.Thread(target=play_song, \
                         args=(get_next_song(), play_callback)\
                         .start()
        while not music_stopped.is_set() and user_not_interrupted() :
            # Spin
            pass

        if music_stopped.is_set() :
            # End of song reached, play the next song
            continue
        else :
            # User input from some source
            process_user_input()

The problem is that when starting a new song after a previous song had played the play_callback would be called as a result of the stop_any_previous_songs() call. When called the play_callback would call set on the music_stopped Event for the next song.

So my question is when the play_callback function is called is it merely going to the enclosing scope and looking for an object with the name `music_stopped'? Or is the underlying cause something else?

Was it helpful?

Solution

    def play_callback() :
        # say that the song stopped
        music_stopped.set()

Python would lookup the value of bare names like music_stopped using the LEGB rule (Locals, Extended, Globals, Builtins). Since music_stopped is not a local variable, Python would next look in the enclosing (lexical) scope. Indeed, it would find music_stopped defined in the enclosing scope of the main function. The value is looked up dynamically -- in other words, the value returned would be the value of music_stopped at the time when the bare name is looked up, i.e. when play_callback is being executed. That value of music_stopped need not be the value at the time when play_callback was defined.

However, to bind music_stopped to the value when play_callback was defined, you could make music_stopped a local variable with a default value:

def play_callback(music_stopped=music_stopped):
    # say that the song stopped
    music_stopped.set()

The default value is evaluated and bound to the local variable at the time when play_callback is defined.

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