Live data from Hacker News

A viable solution for Python concurrency

lwn.net

101–110 of 366 posts

Re: A viable solution for Python concurrency

#101

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"

GOTO cosplaying should go away with structured concurrency (via TaskGroup) being adopted in 3.11, as pioneered by Trio.

Check out anyio if you want to use them now.

Re: A viable solution for Python concurrency

#102
post #93

> 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…

Two spaces in front of each line of the code block. As written, right now, your comment is hard to parse:

  import threading

  def produce():
    global global_foo
    local_foo = "potato"
    global_foo = local_foo

  def consume():
    global global_foo
    local_foo = global_foo
    global_foo = None

  if __name__ == '__main__':
    produce()
    thread = threading.Thread(target=consume)
    thread.start()
    thread.join()

Re: A viable solution for Python concurrency

#103

Earlier quoted context omitted.

If you are ever considering making use of asyncio for your project, I would strongly recommend taking a look at curio [1] as an alternative. It's like asyncio but far, far easier to use. [1] https://curio.readthedocs.io/en/latest/index.html

While the design of Curio is quite interesting, it's may not be a good choice, not for technical reasons, but for logistical reasons: the chances it gets a wide adoption are slim to None. And since we are stuck with colored functions in python, the choice of stack matters very much. Now, if you want easier concurrency, and a solution to a lot of concurrency problems that curio solves, while still being compatible wit…

anyio is also compatible with asyncio and Trio, so you can use it with either library or paradigm.

Re: A viable solution for Python concurrency

#104
post #41

Earlier quoted context omitted.

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_fu…

Oh, top level await... I missed that.

Not sure it will get there, but it would be nice. I think putting a top level "await" is explicit enough for stating you want an event loop anyway.

Now, with TaskGroup in 3.11, things are going to get pretty nice, espacially if this top level await plays out, provided they include async for and async with in the mix.

Now, if they could just make so that coroutines are automatically schedules to the nearest task group, we would almost have something usable.

Re: A viable solution for Python concurrency

#105

> 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…

> which is released (using the same decref code as other threads) when the local reference count goes to zero?

(I may misunderstand your remark, as ‘releasing’ is a bit ambiguous. It could mean decreasing reference count and freeing the memory if the count goes to zero or just plain freeing the memory)

The local ref count can go to zero while other threads still have references to the object (e.g. when the allocating thread sends an object as a message to another thread and, knowing the message arrived, releases it), so freeing the memory when it does would be a serious bug.

Also, the shared ref count can go negative. From the paper:

> As an example, consider two threads T1 and T2. Thread T1 creates an object and sets itself as the owner of it. It points a global pointer to the object, setting the biased counter to one. Then, T2 overwrites the global pointer, decrementing the shared counter of the object. As a result, the shared counter becomes negative.

That can’t happen with the biased counter because, when it would end up going negative, the object gets unbiased, and the shared counter gets decreased instead.

That asymmetry is what ensures that only a single thread updates the biased counter, so that no locks are needed to do that.

Re: A viable solution for Python concurrency

#106
post #86
post #23

Yikes, C extensions can't assume they are under GIL by default: https://github.com/colesbury/numpy/commits/v1.19.3-nogil

It looks like a total of four lines needed changing in numpy due to his change. That's a very good score in my book, numpy is huge.

Unfortunately, every C extension will need to undergo manual review for safety, unless there's some very easy way to have the C extension opt into using the GIL. And some of them will be close to impossible to detangle in this way.

Re: A viable solution for Python concurrency

#107
post #93

> 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 couldn't find this in the design document but the only obvious solution is to track globals via the shared count. Since a global reference is part of all threads simultaneously, it cannot be treated as local.

If you follow this reasoning, the operations above result in local=0/shared=0 after the last assignment.

Re: A viable solution for Python concurrency

#108
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.

Fair point. Async is a big enough idea that it probably warrants designing the language with it in mind. I guess another way of phrasing it would be that it violates the "there's only one way to do it" maxim, and the "two ways of doing it" circumstance necessarily came about because the idea was discovered long after the core language and libraries were already written.

Re: A viable solution for Python concurrency

#109
post #93

> 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…

The paper:

> When the shared counter for an object becomes negative for the first time, the non-owner thread updating the counter also sets the object’s Queued flag. In addition, it puts the object in a linked list belonging to the object’s owner thread called QueuedObjects. Without any special action, this object would leak. This is because, even after all the references to the object are removed, the biased counter will not reach zero — since the shared counter is negative. As a result, the owner would trigger neither a counter merge nor a potential subsequent object deallocation.

> To handle this case, BRC provides a path for the owner thread to explicitly merge the counters called the ExplicitMerge operation. Specifically, each thread has its own thread-safe QueuedObjects list. The thread owns the objects in the list. At regular intervals, a thread examines its list. For each queued object, the thread merges the object’s counters by accumulating the biased counter into the shared counter. If the sum is zero, the thread deallocates the object. Otherwise, the thread unbiases the object, and sets the Merged flag. Then, when a thread sets the shared counter to zero, it will deallocate the object. Overall, as shown in invariant I4, an owner only gives up ownership when it merges the counters.

Well, that works, but it's a bit naff.

Post reply on HN