Live data from Hacker News

A viable solution for Python concurrency

lwn.net

71–80 of 366 posts

Re: A viable solution for Python concurrency

#71

If this effort succeeds (and I hope it does) now Python developers will need to contend with the event-loop albatross of asyncio and all of its weird complexity. In an alternate Python timeline, asyncio was not introduced into the Python standard library, and instead we got a natively supported, robust, easy-to-use concurrency paradigm built around green/virtual threading that accommodates both IO and CPU bound work.

> easy-to-use concurrency paradigm

Well it has queues and threads already.

Its just that asyncio for socket handling at least (in the testing that I did) is about 5% faster. (one asyncio socket "server" vs ten threads [with a number of ways to monitor for new connections])

I always assumed that people wanted asyncio because they look at javascript and thought "hey I want GOTOs cosplaying as a fun paradigm"

Re: A viable solution for Python concurrency

#72
post #20

I'm going to assume that there is a reason that this isn't a switch control, so that the default is a single-threaded program and the programmer needs to state explicitly that this one will be multi-threaded, upon which the interpreter changes into the atomic mode for the rest of execution?

Basically no one would get the glorious single-threaded performance then, since the first time you pip install anything, you're going to discover that it spins up a thread under the hood that you're never exposed to.

Or worse, you end up with the async schism all over again, with new "threadless" versions of popular libraries springing up.

Re: A viable solution for Python concurrency

#73

Earlier quoted context omitted.

Because JavaScript never had threads so I/O in JavaScript has always been non-blocking and the whole ecosystem surrounding it has grown up under that assumption. JavaScript doesn't need a GIL because it doesn't really have threads. WebWorkers are more akin to multiprocessing than threads in Python. Objects cannot be shared directly across WebWorkers so transferring data comes with the expense of serializing/deseriali…

JS now has shared array buffers.

SharedArrayBuffer is just raw memory similar to using mmap from Python multiprocessing. The developer experience is very different to simply sharing objects across threads.

Re: A viable solution for Python concurrency

#74
post #57

Earlier quoted context omitted.

asyncio is not a competition to threads, it's complementary. In fact, it's a perfectly viable strat in python to have several processes, each having several threads, each having an event loop. And it will still be so, once this comes out. You will certainly use threads more, and processes less, but replacing 1000000 coroutines by 1000000 system threads is not necessarily the right strategy for your task. See nginx vs…

Multiple threads with one asyncio loop per thread would be absolutely pointless in Python, because of the GIL. With that said, sure, threads and asyncio are complimentary in the sense that you can run tasks on threadpool executors and treat them as if they were coroutines on an event loop. But that serves no purpose unless you're trying to do blocking IO without blocking your whole process.

I read it as each process having multiple threads and an event loop. If the threads are performing I/O or calling out to compiled code and releasing the GIL, said GIL won't block the event loop.

Re: A viable solution for Python concurrency

#75
post #20

I'm going to assume that there is a reason that this isn't a switch control, so that the default is a single-threaded program and the programmer needs to state explicitly that this one will be multi-threaded, upon which the interpreter changes into the atomic mode for the rest of execution?

Most references are thread local, where this implementation will still beat out atomic refcounts in a multi-threaded app

Re: A viable solution for Python concurrency

#76
post #41

If this effort succeeds (and I hope it does) now Python developers will need to contend with the event-loop albatross of asyncio and all of its weird complexity. In an alternate Python timeline, asyncio was not introduced into the Python standard library, and instead we got a natively supported, robust, easy-to-use concurrency paradigm built around green/virtual threading that accommodates both IO and CPU bound work.

BTW I wonder why async is so painless in ES6 compared to Python. Why the presence of GIL (which JS also has) did not make running async coroutines completely transparent, as it made running generators (which are, well, coroutines already). Why the whole even loop thing is even visible at all.

I used them both extensively, and here are the main reasons I can think of:

- The event loop in JS is invisible and implicit. V8 proved it can be done without paying a cost for it, and in fact most real life python projects are using uvloop because it's faster than asyncio default loop. JS dev don't think of the loop at all, because it's always been there. They don't have to chose a loop, or thinking about its lifecycle or scheduling. The API doesn't show the loop at all.

- Asynchronous functions in JS are scheduled automatically. On python, calling a coroutine function does...nothing. You have to either await it, or pass it to something like asyncio.create_task(). The later is not only verbose, it's not intuitive.

- Async JS functions can be called from sync functions transparently. It just returns a Promise after all, and you can use good old callbacks. Instantiating a Python coroutine does... nothing as we said. You need to schedule it AND await it. If you don't, it may or may not be executed. Which is why asyncio.gather() and co are to be used in python. Most people don't know that, and even if you know, it's verbose, and you can forget. All that, again, because using the event loop must be explicit. That's one thing TaskGroup from trio will help with in the next Python versions...

- the early asyncio API sucked. The new one is ok, asyncio.run() and create_task() with implicit loop is a huge improvement. But you better use 3.7 at least. And you have to think about all the options for awaiting: https://stackoverflow.com/questions/42231161/asyncio-gather-...

- asyncio tutorials and docs are not great, people have no idea how to use it. Since it's more complex, it compounds.

E.G, if you use await:

With node v14.8+:

    await async_func(params)
With python 3.7+:

    import asyncio 

    async def main():
        # no top level await, it must happen in a loop
        await async_func(params) 

    asyncio.run(main) # explicit loop, but easy one thanks to 3.7
E.G, deep inside functions calls, but no await:

With node:

    ...
    async_func(params)

With python 3.7+:

    ...
    # async_func(params) alone would do nothing
    res = asyncio.create_task(async_func(params))

    ...

    # you MAY get away with not using gather() or wait()
    # but you also may get "coroutine is never awaited"
    # RuntimeWarning: coroutine 'async_func' was never awaited
    asyncio.gather(res)
Of course, you could use "run_until_complete()", but then you would be blocking. Which is just not possible in JS, there is one way to do it, and it's always non blocking and easy. Ironic, isn't it? Beside, which Python dev knows all this? I'm guessing most readers of this post will have heard of it for the first time.

Python is my favorite language, and I can live with the explicit loop, but explicit scheduling is ridiculous. Just run the damn coroutine, I'm not instantiating it for the beauty of it. If I want a lazy construct, I can always make a factory.

Now, thanks to the trio nursery concept, we will get TaskGroup in the next release (also you can already use them with anyio):

    async with asyncio.TaskGroup() as tg:
        tg.start_soon(async_func, params)
Which, while still verbose, is way better:

- no gather or wait. Schedule it, it will run or be cleaned up.

- no need to chose an awaiting strat, or learn about a 1000 things. This works for every cases. Wanna use it in a sync call ? Pass the tg reference in it.

- lifecycle is cleanly scoped, a real problem with a lot of async code (including in JS, where it doesn't have a clean solution)

Re: A viable solution for Python concurrency

#77
How big a problem is the possible breakage of C extensions for new code? Is there currently some standard "future proofed for multi-thread" way of writing them that will reduce the odds of the C extension breaking? And maybe also being compatible with PyPy? Or do developers today need to write a separate version for each interpreter that they want to support?

Re: A viable solution for Python concurrency

#78
post #41

If this effort succeeds (and I hope it does) now Python developers will need to contend with the event-loop albatross of asyncio and all of its weird complexity. In an alternate Python timeline, asyncio was not introduced into the Python standard library, and instead we got a natively supported, robust, easy-to-use concurrency paradigm built around green/virtual threading that accommodates both IO and CPU bound work.

BTW I wonder why async is so painless in ES6 compared to Python. Why the presence of GIL (which JS also has) did not make running async coroutines completely transparent, as it made running generators (which are, well, coroutines already). Why the whole even loop thing is even visible at all.

> Why the whole even loop thing is even visible at all.

It isn't anymore.

    In [3]: from asyncio import run

    In [4]: async def async_func(): print('Ran async_func()')

    In [5]: run(async_func())
    Ran async_func()
Top-level async/await is also available in the Python REPL and IPython, and there are discussions on the Python mailing list about making top-level async/await the default for Python[1].

    In [1]: async def async_func(): print('Ran async_func()')

    In [2]: await async_func()
    Ran async_func()

[1] https://groups.google.com/g/python-ideas/c/PN1_j7Md4j0/m/0xy...

Re: A viable solution for Python concurrency

#79
post #44

> With this scheme, the reference count in each object is split in two, with one "local" count for the owner (creator) of the object and a shared count for all other threads. Since the owner has exclusive access to its count, increments and decrements can be done with fast, non-atomic instructions. Any other thread accessing the object will use atomic operations on the shared reference count. > Whenever the owning th…

I like this idea. In fact another possibility is to have a thread-local reference count for each thread that uses the object which can use fast non-atomic operations, and then each thread can use a shared atomic reference count, that counts how many threads use the object. When each thread-local count goes to zero, the shared count is decremented by one. This way, if an object is created in one thread and transferred…

How would the storage be laid out?

With the proposed scheme, there are two counters (and, i assume, the ID of the owning thread), so a small fixed-size structure, which can sit directly in the object header. With your scheme, you need a variable and unbounded number of counters. Where would they go?

Re: A viable solution for Python concurrency

#80
post #19

Earlier quoted context omitted.

What specifically is the problem with asyncio? I quite like using it, so I'm curious if there's some aspect that makes it unsustainable?

The key disadvantage is largely that it bifurcates the library base. Async libraries and sync libraries co-exist uneasily in the same program. For nearly every popular library there is now a (usually inferior, less robust) async one. The benefits of Linus' Law are reduced.

Trifucates, since now there’s stdlib asyncio, and a popular trio async flavor too.
Post reply on HN