With async being all the rage these days, I’m a bit surprised that there hasn’t been more of an rumble over the fact that the GIL is still broken. No, I’m not talking about it’s removal entirely. I’m referring to the fact that launching a computationally intensive thread is an easy way to kill the responsiveness of an event loop almost entirely. Let’s say you’ve got a little asyncio code like this:
import asyncio async def echo_client(reader, writer): addr = writer.get_extra_info('peername') print('Connection from', addr) while True: data = await reader.read(100000) if not data: break writer.write(data) await writer.drain() print('Connection closed') if __name__ == '__main__': loop = asyncio.get_event_loop() coro = asyncio.start_server(echo_client, '127.0.0.1', 25000, loop=loop) loop.run_until_complete(coro) loop.run_forever()
Running this on a little benchmark, I see it taking about 16000 requests/second. The exact details don’t matter, but let’s kill the performance. Just modify the code to launch a CPU-intensive thread:
... def fib(n): if n < 2: return 1 else: return fib(n-1) + fib(n-2) if __name__ == '__main__': import concurrent.futures executor = concurrent.futures.ThreadPoolExecutor() loop = asyncio.get_event_loop() coro = asyncio.start_server(echo_client, '127.0.0.1', 25000, loop=loop) loop.run_in_executor(executor, fib, 50) loop.run_until_complete(coro) loop.run_forever()
Now the performance drops down to about 135 requests/second. Excellent. For those of you keeping score, that’s only a drop by a factor of about 120 (Just as a note, my Curio project is negatively affected in exactly the same manner).
So why does it happen? Remember my GIL Talk from 2010 and the fact that the GIL was “fixed” in Python 3.2? Well, it wasn’t fixed. It was merely changed into something else. More details about what’s happening can be found in my RuPy’2011 talk.
I sometimes wonder if it’s time to revisit this issue and the idea of thread priorities ;-).