Live data from Hacker News

What Python’s asyncio primitives get wrong about shared state

inngest.com

51–55 of 55 posts

Re: What Python’s asyncio primitives get wrong about shared state

#51
post #47

Earlier quoted context omitted.

> The pattern for fire-and-forget is impossible Good, that's an antipattern in the coroutines concurrency model.

Then someone should really update the official python docs that explain the fire-and-forget pattern ( https://docs.python.org/3/library/asyncio-task.html#asyncio.... )! I had a FastAPI server, and calling a particular endpoint is supposed to kick off some work in the background. The background work does very little CPU work, but does often need to await more work for several minutes, so it's a good fit for asyncio. H…

Concurrency based on coroutines is making use of cooperative multitasking, leaving the concurrent execution of tasks up to the runtime. To elaborate a bit on what that implies, let me just ask you the following question: Is there something less cooperative than a task that doesn't yield its control back to the main thread?

Regarding the fire-and-forget pattern I can think of at least two issues, let me illustrate them with the following example:

  import asyncio
  from typing import Any, Coroutine

  _background_tasks = set[asyncio.Task[None]]()

  def _fire_and_forget(coro: Coroutine[Any, Any, None]) -> None:
    task = asyncio.create_task(coro)
    _background_tasks.add(task)
    task.add_done_callback(_background_tasks.discard)

  async def _run_a() -> None:  # to illustrate issue A
    raise RuntimeError()

  async def _run_b() -> None:  # to illustrate issue B
    await asyncio.sleep(1)
    print('done')

  async def main() -> None:
    # issue A: Exceptions can't be caught
    try:
      _fire_and_forget(_run_a())
    except RuntimeError as exc:
      print(f'Exception caught: {exc}')

    # issue B: Task won't complete
    _fire_and_forget(_run_b())

  if __name__ == '__main__':
    asyncio.run(main())
Feel free to comment out either task in the main function to observe the resulting behaviors individually.

For issue A: Any raised error in the background task can't be caught and will crash the running main thread (the process)

For issue B: Background tasks won't be completed if the main thread comes to a halt

With the decision for the fire-and-forget pattern you'll make a deliberate choice to leave any control of the runtime up to blind chance. So from an engineering POV that pattern isn't a solution-pattern to some real problem, it's rather a problem-pattern that demands a reworked solution.

> How do you want it to be structured

Take a look at the caveats for FastAPI/Starlette Background Tasks: https://fastapi.tiangolo.com/tutorial/background-tasks/#cave...

Losing control of a background task (and therefor the runtime) might be fine for some demo project, but I think you'll want to notice raised errors in any serious production system, especially for any work that takes several minutes to complete.

Re: What Python’s asyncio primitives get wrong about shared state

#52
post #47

Earlier quoted context omitted.

Then someone should really update the official python docs that explain the fire-and-forget pattern ( https://docs.python.org/3/library/asyncio-task.html#asyncio.... )! I had a FastAPI server, and calling a particular endpoint is supposed to kick off some work in the background. The background work does very little CPU work, but does often need to await more work for several minutes, so it's a good fit for asyncio. H…

Concurrency based on coroutines is making use of cooperative multitasking, leaving the concurrent execution of tasks up to the runtime. To elaborate a bit on what that implies, let me just ask you the following question: Is there something less cooperative than a task that doesn't yield its control back to the main thread? Regarding the fire-and-forget pattern I can think of at least two issues, let me illustrate the…

> Is there something less cooperative than a task that doesn't yield its control back to the main thread? Of course it does yield back to the main thread in my example, at each await point, just like any other cooperative task.

In my case, I specifically want an independent execution of a task. Admittedly, it has to catch its own exceptions and deal with them, as you pointed out, because that's part of being independent.

(Technically, in issue A it doesn't crash the running thread. The event loop catches the exception, but it complains later when the task is garbage collected. Issue B is fine for my use - when the event loop shuts down, it cancels remaining tasks, which is exactly right for my server.)

Re: What Python’s asyncio primitives get wrong about shared state

#53
post #19

Earlier quoted context omitted.

Async and await is manually scheduling threads. So, if you're quite careful about what functions you call, you can arrange things so that you don't get concurrency when you don't want it. Being careful about what functions you call is quite fragile and tedious, and doesn't compose well: what if a library changes when it adds a yield point? Overall, async/await is a result of people programming like it's 2003, when th…

Threads are still expensive in Python - can’t use them for concurrency really like you can with async io afaik.

I would be surprised if they were particularly expensive. There's a GIL, so you don't get concurrency benefits -- but that mainly makes them behave like async.

Re: What Python’s asyncio primitives get wrong about shared state

#54
post #14

The one thing I wish stock python queues had an option for (async or otherwise) was some kind of explicit termination. e.g. be split into producers and consumers, and have consumers indicate iteration complete when all producers have finished (and vice versa - signal producers that all consumers have gone away). You can kind of kludge around it in one direction with stop sentinals but it's a lot more awkward to deal…

Does task_done not do what you want? https://docs.python.org/3/library/queue.html#queue.Queue.tas...

Not really. It's certainly intended for the basic "fan out m tasks to n workers, and the fanout producer wants to know when they're all done" and can be abused for some more, but I don't think it does anything to help with the "consumer died, I want the producers to be able to know this rather than just continuing to push messages into a queue forever" case.

I've written wrappers to handle things the way I want, but it always feels like a bit of a hack. (Usually I use a stop sentinal internally and reach inside to unbound the queue before I send it to avoid blocking). Just wish it were built in.

Re: What Python’s asyncio primitives get wrong about shared state

#55
post #43
post #42

Earlier quoted context omitted.

> Python's asyncio library is single threaded, so I'm not sure why you are talking about threads and asyncio like they have anything to do with each other. Ok, not OS threads, but it de facto creates application/green threads. >That's what the multiprocess library is for. It's not an ideal solution, but it does exist. Philosophical argument but, I'd say multiprocess is not python doing many things, there would be man…

It absolutely does not create green threads. Green threads can be preempted and switched by the runtime. Go does this for example. Python's asyncio is tasked based, the event loop cannot switch out a task until it reaches a yield point. Or in short, green threads like Go uses are preemptive multitasking, the task based model asyncio uses is cooperative. A CPU bound python task can block the event loop forever if it n…

>the task based model asyncio uses is cooperative. A CPU bound python task can block the event loop forever if it never yields, goroutines generally can't.

I think I was wrong then. The difference between:

a = threading.Thread(request,url1).start()

b = threading.Thread(request,url2).start()

a.join()

b.join()

With similar asyncio code is that async code has to explicitly signal when the task is allowed to switch. So async is used for when greater control is required over when the scheduler can work on the two different tasks, presumably to avoid race conditions. It would be used in cases similar to where semaphores would have been used.

I do still think that it's unpythonic in that whatever you can do with async you can do without, (two ways to do things), and most of the usecases of this will be people coming from node, and people who don't know of more basic concurrent techniques.

I'm looking at the Original Article again, and it just looks like they are implementing a more complex pub-sub control flow system instead of using if statements (because that's too boring?). The traditional solution would use a socket which is essentially a thread, and the states of the connection would just be managed by TCP instead of recreated at the application layer.

I just can't shake the notion that the vast majority of cases async code in python is bad code. Of course the devs are not idiots, but my thesis is that they are bored and have to implement something, and it has an intended use case, but the bulk of usage will be for the 'wrong' reasons.

>You're conflating several different distinct ideas. It's a common mistake I see at work all the time. Took a good amount of reading for me to untangle them all.

If you have a source material to recommend on the subject (assuming I am already aware on traditional OS scheduling of processes, threads and green threads) I would be interested in reading that. It seems that even if my thesis is True, I need to understand for what precise usecases BDFL and company are developing this async thing into the language itself.

Post reply on HN