Non-blocking I/O with asyncio

Question:

I’m trying to write a networked game with Pygame and asyncio, but I can’t work out how to avoid hanging on reads. Here is my code for the client:

@asyncio.coroutine
def handle_client():
    print("Connected!")
    reader, writer = yield from asyncio.open_connection('localhost', 8000)
    while True:
        mouse_up = False
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()                
            elif event.type == pygame.MOUSEBUTTONUP:
                mouse_up = True

        if mouse_up:
            print("Writing")
            writer.write(b"Mouse up")
        print("Waiting to read")
        line = yield from reader.read(2**12)
        print(line.decode())

    writer.close()

This hangs on the line line = yield from reader.read(2**12). I previously thought that the point of asyncio was that it was non-blocking, and so if there wasn’t any data to read it would just continue executing. I see now that this isn’t the case.

How do I integrate the asyncio networking code with the Pygame drawing and event code?

Asked By: rlms

||

Answers:

The point of yield from is to switch the execution to the asyncio’s event loop and to block the current coroutine until the result is available. To schedule a task without blocking the current coroutine, you could use asyncio.async().

To print read-so-far data without blocking the pygame loop:

@asyncio.coroutine
def read(reader, callback):
    while True:
        data = yield from reader.read(2**12)
        if not data: # EOF
            break
        callback(data)

@asyncio.coroutine
def echo_client():
    reader, ...
    chunks = []
    asyncio.async(read(reader, chunks.append))
    while True:
        pygame.event.pump() # advance pygame event loop
        ...
        if chunks: # print read-so-far data
            print(b''.join(chunks).decode())
            del chunks[:]
        yield from asyncio.sleep(0.016) # advance asyncio loop

There should be no blocking calls inside the while loop.

read() and sleep() coroutines run concurrently in the same thread (obviously you could run other coroutines concurrently too).

Answered By: jfs

You can “transform” a blocking task into a non-blocking one.

I suggest this: https://docs.python.org/3/library/asyncio-eventloop.html#executor.

I have a function that listens to a twitter feed, function “mention”, and I run it in an executor, so if it hangs, it doesn’t block the other tasks.

@asyncio.coroutine
def boucle_deux():
#faire attendre la boucle si pas bcp de mots
    while True:
        print("debut du deux")
        value = t.next()
        future2 = loop.run_in_executor(None, mention, "LQNyL2xvt9OQMvje7jryaHkN8",
                                       "IRJX6S17K44t8oiVGCjrj6XCVKqGSX9ClfpGpfC467rajqePGb",
                                       "2693346740-km3Ufby8r9BbYpyzcqwiHhss22h4YkmnPN4LnLM",
                                       "53R8GAAncFJ1aHA1yJe1OICfjqUbqwcMR38wSqvbzsQMB", 23, value)
        response2 = yield from future2
        yield from asyncio.sleep(5)
        print("fin du deux")

asyncio.Task(boucle_deux())
Answered By: Morgan

well since you are trying to read the value of ‘line’ right after you call read() you need that value at any cost…

if the coroutine wouldn’t stop cause there are no data, you could get an AttributeError on the line.decode() call if ‘line’ then is None.

one thing you can do is to set a timeout on the blocking call and handle the timeout exception:

...
print("Waiting to read")
try:  # block at most for one second
    line = yield from asyncio.wait_for(reader.read(2**12), 1)
except asyncio.TimeoutError:
    continue
else:
    print(line.decode())
...
Answered By: Danny