1

I am writing some code where I have 3 processes (spawned from the main). The first one is a process that uses Async IO to create 3 coroutines and switch between them. The last two processes run independently and generate two outputs that are used in one of the coroutines of the first process.

The communication has been managed using multiprocessing.queue(), the main puts the input data inside queue_source_position_hrir_calculator and queue_source_position_cutoff_calculator, then these two queues are emptied by p2_hrir_computation_process and p3_cutoff_computation_process. These two processes outputs their computation results in two output queues queue_computed_hrirs and queue_computed_cutoff

Finally these two queues are consumed by the Async IO process, in particular inside the input_parameters_coroutine function.

The full code is the following (I will highlight the key parts in following snippets):

import asyncio
import multiprocessing
import numpy as np
import time

from classes.HRIR_interpreter_min_phase_linear_interpolation import HRIR_interpreter_min_phase_linear_interpolation
from classes.object_renderer import ObjectRenderer

#Useful resources: https://bbc.github.io/cloudfit-public-docs/asyncio/asyncio-part-2
#https://realpython.com/async-io-python/
Fs = 44100

# region Async_IO functions
async def audio_input_coroutine(overlay):

    for i in range(0,100):
        print('Executing audio input coroutine')
        print(overlay)

        await asyncio.sleep(1/(Fs*4))

async def input_parameters_coroutine(overlay, queue_computed_hrirs,queue_computed_cutoff):

    for i in range(0,10):
        print('Executing audio input_parameters coroutine')
        #print(overlay)
        current_hrir = queue_computed_hrirs.get()
        print('got current hrir')

        current_cutoff = queue_computed_cutoff.get()
        print('got current cutoff')

        await asyncio.sleep(0.5)


async def audio_output_coroutine(overlay):

    for i in range(0,10):
        print('Executing audio_output coroutine')
        #print(overlay)
        await asyncio.sleep(0.5)



async def main_coroutine(overlay, queue_computed_hrirs,queue_computed_cutoff):
    await asyncio.gather(audio_input_coroutine(overlay), input_parameters_coroutine(overlay, queue_computed_hrirs,queue_computed_cutoff), audio_output_coroutine(overlay))

def async_IO_main_process(queue_computed_hrirs,queue_computed_cutoff):
    overlay = 10
    asyncio.run(main_coroutine(overlay, queue_computed_hrirs,queue_computed_cutoff))
# endregion

# region HRIR_computation_process

def compute_hrir(queue_source_position, queue_computed_hrirs):
    print('computing hrir')
    SOFA_filename = '../HRTF_data/HUTUBS_min_phase.sofa'
    # loading the simulated dataset using the support class HRIRInterpreter
    HRIRInterpreter = HRIR_interpreter_min_phase_linear_interpolation(SOFA_filename=SOFA_filename)

    # variable to check if I have other positions in my input queue
    eof_source_position = False
    # Un-comment following line to return when no more messages
    while not eof_source_position:
    #while True:
        # print('inside while loop')
        time.sleep(1)
        # print('state of the queue', queue_source_position.empty())

        if not eof_source_position:
            position = queue_source_position.get()
            if position is None:
                eof_source_position = True  # end of messages indicator
            else:
                required_IR = HRIRInterpreter.get_interpolated_IR(position[0], position[1], 1)
                queue_computed_hrirs.put(required_IR)
                # print('printing computed HRIR:', required_IR)

    print('completed hrir computation, adding none to queue')
    queue_computed_hrirs.put(None)  # end of messages indicator
    print('completed hrir process')

# endregion

# region cutoff_computation_process

def compute_cutoff(queue_source_position, queue_computed_cutoff):
    print('computing cutoff')
    cutoff = 20000
    object_renderer = ObjectRenderer()

    object_positions = np.array([(20, 0), (40, 0), (100, 0), (225, 0)])

    eof_source_position = False
    # Un-comment following line to return when no more messages
    while not eof_source_position:
    #while True:
        time.sleep(1)
        object_renderer.update_object_position(object_positions)

        if not eof_source_position:
            print('inside source position update')
            source_position = queue_source_position.get()
            if source_position is None:  # end of messages indicator
                eof_source_position = True
            else:
                cutoff = object_renderer.get_cutoff(azimuth=source_position[0], elevation=source_position[1])

        queue_computed_cutoff.put(cutoff)

    queue_computed_cutoff.put(None)  # end of messages indicator

# endregion

if __name__ == "__main__":
    import time

    queue_source_position_hrir_calculator = multiprocessing.Queue()
    queue_source_position_cutoff_calculator = multiprocessing.Queue()

    queue_computed_hrirs = multiprocessing.Queue()
    queue_computed_cutoff = multiprocessing.Queue()

    i = 0.0
    #Basically here I am writing a sequence of positions into the queue
    #then I add a None value to detect when I am done with the simulation so the process can end
    for _ in range(10):
        # print('into main while-> source_position:', source_position[0])
        source_position = np.array([i, 0.0])
        queue_source_position_hrir_calculator.put(source_position)
        queue_source_position_cutoff_calculator.put(source_position)
        i += 10

    queue_source_position_hrir_calculator.put(None)  # "end of messages" indicator
    queue_source_position_cutoff_calculator.put(None)  # "end of messages" indicator

    p1_async_IO_process = multiprocessing.Process(target=async_IO_main_process, args=(queue_computed_hrirs,queue_computed_cutoff)) #process that manages the ASYNC_IO coroutines between DMAs
    p2_hrir_computation_process = multiprocessing.Process(target=compute_hrir,  args=(queue_source_position_hrir_calculator, queue_computed_hrirs))
    p3_cutoff_computation_process = multiprocessing.Process(target=compute_hrir,  args=(queue_source_position_cutoff_calculator, queue_computed_cutoff))

    p1_async_IO_process.start()
    p2_hrir_computation_process.start()
    p3_cutoff_computation_process.start()

    #temp cycle to join processes
    #for _ in range(2):
    #    current_hrir = queue_computed_hrirs.get()
    #    current_cutoff = queue_computed_cutoff.get()

    print('joining async_IO process')
    p1_async_IO_process.join()
    print('joined async_IO process')

    #NB: to join a process, its qeues must be empty. So before calling the join on p2, I should get the values from the queue_computed_hrirs queue
    print('joining hrir computation process')
    p2_hrir_computation_process.join()
    print('joined hrir computation process')

    print('joining hrir computation process')
    p2_hrir_computation_process.join()
    print('joined hrir computation process')

    print('joining cutoff computation process')
    p3_cutoff_computation_process.join()
    print('joined cutoff computation process')

    print("completed main")

The important part of the code is:

async def input_parameters_coroutine(overlay, queue_computed_hrirs,queue_computed_cutoff):

    for i in range(0,10):
        print('Executing audio input_parameters coroutine')
        #print(overlay)
        current_hrir = queue_computed_hrirs.get()
        print('got current hrir')

        current_cutoff = queue_computed_cutoff.get()
        print('got current cutoff')

        await asyncio.sleep(0.5)

This coroutine receives as input 3 variables overlay (which is a dummy variable I am using for future developments) and the two multiprocessing.Queue() classes, queue_computed_hrirs and queue_computed_cutoff.

At the moment my input_parameters_coroutine gets "stuck" while executing current_hrir = queue_computed_hrirs.get() and current_cutoff = queue_computed_cutoff.get(). I said "stuck" because the code works fine and complete its execution, the problem is that those two commands are blocking, thus my coroutine stops until it has something to get from the queue.

What I would like to achieve is: try to execute current_hrir = queue_computed_hrirs.get(), if it is not possible at that moment, switch to another coroutine and let it execute what it wants, then go back and check if it possible to execute current_hrir = queue_computed_hrirs.get(), if yes go on, if not switch again to another coroutine and let it do its job.

I saw that there are some problems in making async IO and multiprocessing communicate ( What kind of problems (if any) would there be combining asyncio with multiprocessing? , Can I somehow share an asynchronous queue with a subprocess? ) but I wasn't able to find a smart solution to my problem.

Mattia Surricchio
  • 1,362
  • 2
  • 21
  • 49
  • 2
    Try `current_hrir = await asyncio.get_event_loop().run_in_executor(None, queue_computed_hrirs.get)` – user4815162342 Aug 24 '21 at 11:41
  • I came across the same answer in some other question (I can not find it right now...) and if i don't remember wrong there were some possible problems with scalability and number of threads, am i wrong? – Mattia Surricchio Aug 24 '21 at 11:45
  • Found it: https://stackoverflow.com/questions/60622320/multiprocessing-process-and-asyncio-loop-communication – Mattia Surricchio Aug 24 '21 at 11:47
  • As far as I can tell, you invoke only a single instance of `input_parameters_coroutine`, so the scalability limitations don't apply to your case. Most uses of `run_in_executor` are a compromise (using a thread pool to create an async bridge towards inherently non-async code), but when you are using the multi-threaded queue which doesn't support async, you're not left with many other options. There are packages that purport to provide an async interface to multi-processing, but AFAIR all of them use exactly the same approach (a thread pool) under the hood. – user4815162342 Aug 24 '21 at 12:05
  • @user4815162342 Thank you for the clarifications! I was worried because this code is supposed to run on an embedded system, so performances matter a lot. So this is the only workaround to make async io and multiprocessing communicate? – Mattia Surricchio Aug 24 '21 at 12:13
  • 1
    It's the only one I know of, but I haven't checked recently, so you might want to google around in case someone wrote a better one. – user4815162342 Aug 24 '21 at 12:31
  • I am going with your solution for the moment, but I will wait for someone to provide a different (if exists) solution! I have already googled a lot, but I didn't find any solution besides using other libraries such as https://github.com/dano/aioprocessing , which I honestly would like to avoid – Mattia Surricchio Aug 24 '21 at 12:34
  • 1
    Also, aioprocessing has the exact kind of library I alluded to in the last comment - using `run_in_executor` under the hood. To be fair, they don't hide that at all, in fact they're quite transparent about it. Under "How does it work" they say: "In most cases, this library makes blocking calls to multiprocessing methods asynchronous by executing the call in a `ThreadPoolExecutor`, using `asyncio.run_in_executor()`. It does not re-implement multiprocessing using asynchronous I/O. [...]" So they do what the suggested snippet is doing, with the added cost of a complex new dependency. – user4815162342 Aug 24 '21 at 12:38

0 Answers0