[HN Gopher] A Heisenbug lurking in async Python
       ___________________________________________________________________
        
       A Heisenbug lurking in async Python
        
       Author : willm
       Score  : 640 points
       Date   : 2023-02-11 17:25 UTC (1 days ago)
        
 (HTM) web link (textual.textualize.io)
 (TXT) w3m dump (textual.textualize.io)
        
       | dataflow wrote:
       | The notion of fire-and-forget is itself the problem. Even with
       | threads, you should have them join the main thread before the
       | program exits. Which implies you should hold strong references to
       | them until then. Most people don't go out of their way to do this
       | even when they're able to, but that's what you're supposed to do.
        
         | SamReidHughes wrote:
         | I came here to write this comment. Also, you usually need to
         | have some means of canceling the task -- otherwise you have to
         | wait for them to finish, or you leak these stray lost tasks
         | that are doing stuff, like manipulating the state of things.
        
         | Too wrote:
         | This. Even if you hold a reference to the task, your program
         | very likely has a bug. At some point you should always await it
         | to see if it failed or not.
         | 
         | It's easy to miss this if you observe completion via a side-
         | channel, for example item removed from a queue. But this is
         | also a bad way to write tasks in the first place, let them
         | return meaningful data rather than mutate shared objects. That
         | way you are forced to await them and your code becomes much
         | more straightforward. It's counter-intuitive at first if you
         | think in threads, because there you are more used to worker
         | pools and such, whereas asyncio tasks can be written in a more
         | linear way and don't background workers to the same extent.
         | 
         | After having done this mistake several times, I've concluded
         | one should almost never use create_task. It's much better to
         | place them into a top-level list of background tasks, that is
         | always awaited, using this method they are both started
         | automatically and always awaited for errors appropriately.
        
           | dataflow wrote:
           | > At some point you should always await it to see if it
           | failed or not.
           | 
           | What you're sayin is correct, but doesn't _quite_ imply what
           | I 'm saying. I'm saying _everything_ that you spawn
           | asynchronously (be they threads, tasks, whatever) needs to be
           | joined - even if they 're no-ops whose success or failure is
           | irrelevant. This is similar to how you should always make
           | sure to deallocate memory that you dynamically allocate
           | whenever you can, as a matter of good practice and good
           | hygiene. Sometimes you can get away with not doing so, but
           | you shouldn't really skip it unless you don't have a choice,
           | as it makes the program logic clearer and can make the
           | program more robust too. (e.g., imagine running your main()
           | in a loop where threads are spawned each time but never
           | guaranteed to join.)
        
         | sidlls wrote:
         | Conveniences like this library and other threading libraries
         | make it easy for people to trivialize something (concurrent
         | programming) that ought not be.
        
         | fernandotakai wrote:
         | one of the reasons i was never "bitten" by this bug: whenever i
         | use python tasks, i save them in a collection so i can cancel
         | all of them when the program is ready to quit.
         | 
         | it makes cleaning up after myself a LOT easier.
        
       | bornfreddy wrote:
       | Wow. What a strange design decision, as evidenced by sheer number
       | of developers who don't / didn't know about this (myself
       | included). I hope this gets _fixed_ instead of just documented.
        
         | jcheng wrote:
         | Agreed, I'm really surprised at all the comments defending this
         | behavior. I suspect there is a non-obvious reason why it's this
         | way, but "you should've read the docs" and "but why _wouldn't_
         | you hold your own strong reference" are weird takes IMHO.
        
           | hitekker wrote:
           | Bugs stemming from the architecture of a poorly specified
           | system become insecurities for the people who rely on that
           | system.
           | 
           | One of the major reasons why Python's leadership refused to
           | optimize python's performance, besides Guido's intransigence,
           | was because they treated the CPython implementation as the
           | specifications.
           | 
           | Legions of script kiddies built their programming identities
           | around the belief that python must be slow, because to admit
           | otherwise would require changing the system.
        
         | remram wrote:
         | Not just that, it is also annoying to work around. Sometimes
         | you really do want to kick off a background task and have it
         | run to completion, even if you can't keep a handle on it, just
         | like a thread. This is surprisingly hard to do.
        
           | vore wrote:
           | You can always do something extremely goofy like this (but
           | you shouldn't):                   _running_tasks = set()
           | def my_create_task(coro, **kwargs):             async def
           | _coro():                 nonlocal task                 try:
           | return await coro                 finally:
           | _running_tasks.remove(task)                  task =
           | asyncio.create_task(_coro(), **kwargs)
           | _running_tasks.add(task)             return task
        
             | remram wrote:
             | I have a very similar helper in multiple codebases in
             | production. It has some additional bits to log errors and
             | to handle tasks that should never complete (basically a
             | flag to call sys.exit if they do) but it's basically this.
             | 
             | I don't understand why theads get a background mode but
             | tasks can't be fire-and-forget.
        
       | boomskats wrote:
       | As someone who happens to be eternally grateful to the author for
       | his contribution to the Python ecosystem [0], I kinda feel like
       | this comment thread is overreacting to his overreaction. When I
       | look at this post all I see is a useful, well explained, byte-
       | size writeup that a search engine might recommend to someone
       | looking for help in writing async Python.
       | 
       | Maybe it's because a bunch of my friends are Scottish and I get
       | their sense of humour.
       | 
       | [0]: https://rich.readthedocs.io/ (yes I'm talking about the
       | fancy new progress bar that pip got recently)
        
         | quietbritishjim wrote:
         | I do wish he dwelt on task groups a bit more at the end. Many
         | comments here seemed too have missed that bit. They're not just
         | a handy way of executing a hack. Instead, they're a
         | revolutionary way (ok maybe that's a but string but not much)
         | to structure your async program to avoid a whole host of bugs.
         | 
         | A code snippet would have been nice, or a link to the blog post
         | that introduced them (in trio, another async library):
         | https://vorpus.org/blog/notes-on-structured-concurrency-or-g...
        
           | ravloony wrote:
           | I also came here to say this. Structured concurrency gets rid
           | of these types of problems.
        
           | quietbritishjim wrote:
           | *bit strong
        
         | vore wrote:
         | To me, the surprise here is that usually you don't expect
         | Python finalizers to do something like this: when they dispose
         | something, it's usually unobservable from the perspective of
         | the program, e.g. an unreachable file descriptor. Here, the
         | runtime is disposing something that is still observably in
         | progress, which is surprising behavior.
        
           | [deleted]
        
       | rlpb wrote:
       | This issue doesn't exist with Trio's structured concurrency
       | model. In other words, the problem is already solved.
        
         | nbadg wrote:
         | I'll +1 the Trio shoutout [1], but it's worth emphasizing that
         | the core concept of Trio (nurseries) now exists in the stdlib
         | in the form of task groups [2]. The article mentions this very
         | briefly, but it's easy to miss, and I wouldn't describe it as a
         | solution to this bug, anyways. Rather, it's more of a different
         | way of writing multitasking code, which happens to make this
         | class of bug impossible.
         | 
         | [1] https://github.com/python-trio/trio
         | 
         | [2] https://docs.python.org/3/library/asyncio-task.html#task-
         | gro...
        
           | Tanjreeve wrote:
           | Oh good so now we can all move to this years Async flavour in
           | Python.
        
       | mhils wrote:
       | The note in the official Python documentation was only added in
       | September 2022 [1], so no wonder this comes as a surprise to
       | many!
       | 
       | [1]
       | https://github.com/python/cpython/commit/6281affee6423296893...
        
       | edfletcher_t137 wrote:
       | This is a great blog post. Concise, lacking fluff or extraneous
       | prose, it gets right to the point, presents the primary-source
       | reference and then gets right to the solution. A bit of
       | editorializing in the middle but that's completely allowed when
       | writing this tightly. Well damn done, OP.
       | 
       | And also it's _great_ information that I - like I 'm sure many of
       | you - also never noticed. THANK YOU!
        
         | [deleted]
        
         | mgsk wrote:
         | What does this add this isn't already right there in the
         | documentation?
        
           | nkrisc wrote:
           | If there was nothing to add then there wouldn't be loads of
           | projects on GitHub making exactly this mistake.
        
           | Jtsummers wrote:
           | It draws attention to a problem that a lot of people have
           | created for themselves by not reading the documentation (or
           | not recalling it if they read it). I guess the author could
           | have just linked the documentation but then they couldn't
           | have added the additional context of the github search
           | demonstrating how common it is.
        
             | newaccount74 wrote:
             | I must have looked through the docs for create_task a dozen
             | times while trying to figure out how async/await works in
             | Python but still managed to overlook this part.
        
               | edflsafoiewq wrote:
               | That is unsurprising. It was first added as a brief note
               | only in 3.9, and expanded to its present length only in
               | 3.10.
        
               | wnolens wrote:
               | Same.
        
           | klyrs wrote:
           | The author doesn't go into much detail on that point: this
           | warning should be present in documentation of many Python
           | libraries that use create_task and return the result to the
           | user unless that library stores those tasks in a collection
           | as is recommended -- at which point the library author had
           | better roll their own garbage collection!
        
         | isoprophlex wrote:
         | Well, I don't know, I kinda miss the human angle. I'd have
         | loved to first read six paragraphs about how the author's
         | grandmother raised them on home grown threads and greenlets :^)
        
           | nickjj wrote:
           | > I'd have loved to first read six paragraphs about how the
           | author's grandmother raised them on home grown threads and
           | greenlets.
           | 
           | With recipes, often times your problem is you want to learn
           | how to make something where having the steps listed out is
           | the most important thing. The story behind the recipe isn't
           | important to solve your problem but for tech the story around
           | the choice is important. Often times the "why" is really
           | important and I really like hearing about what led someone to
           | use something first. Often times that's more important or
           | equally as important as the implementation details.
           | 
           | It wouldn't make sense for this post given its title but if
           | someone were making a post about why they chose to use async
           | in Python I'd expect and hope that half of the post goes into
           | the gory details of how they tried alternatives and what
           | their shortcomings were for their specific use cases. That
           | would help me as the reader generalize their post to my
           | specific use cases and see if it applies.
        
             | bialpio wrote:
             | Off-topic but the life story is there to make them eligible
             | to be protected by copyright. IANAL.
             | 
             | Source: https://copyrightalliance.org/are-recipes-
             | cookbooks-protecte...
        
               | iudqnolq wrote:
               | For some reason whenever this comes up there'll be one
               | person saying "I bet you didn't know it's for copyright"
               | and another saying "I bet you didn't know it's for SEO".
               | I've yet to see either prove anything beyond that it's a
               | plausible explanation that could fit the minimal known
               | facts.
        
               | jackthetab wrote:
               | I can't find the reference atm, but Jeff Jarvis
               | (https://en.wikipedia.org/wiki/Jeff_Jarvis) says that
               | this _is_ to get around copyright law; the same technique
               | was being done over a hundred years ago (possibly more,
               | that's why I want to reference!). Instead of blogs, think
               | pamphlets.
               | 
               | The More You Know...
        
               | gigatexal wrote:
               | Really? Hmm. Had no idea
        
               | flandish wrote:
               | Interesting. I always thought it was search engine
               | optimization.
        
               | aidenn0 wrote:
               | SEO is definitely a big part of it; Google penalized
               | pages where people closed or navigated away quickly.
        
               | fbdab103 wrote:
               | I immediately bounce from those Stackoverflow clones that
               | keep appearing up at the top of searches. So, I am
               | wondering how much this is still weighted in the scores.
        
               | gdprrrr wrote:
               | https://github.com/quenhus/uBlock-Origin-dev-filter
        
               | jonas21 wrote:
               | You might. But many people don't. They just want an
               | answer and don't care if it's a clone or not.
        
               | [deleted]
        
               | kevin_thibedeau wrote:
               | Leaked Google code:                 if(bounced &&
               | hosts_doubleclick_ads) pagerank++;
        
               | rmbyrro wrote:
               | SEO makes total sense. I always add grandma keywords when
               | I'm searching for Python stuff on Google.
               | 
               | Like: "grandma, how the hell have I still not memorized
               | the API and keep needing to resort to the same doc pages
               | again and again?"
               | 
               | Now I trained ChatGPT with grandma letters from when I
               | was young, so it will answer just like if it was my
               | grandma.
        
               | water-your-self wrote:
               | Its engagement optimization. Adsense pays more if you
               | spend more time on the page
        
               | bialpio wrote:
               | No reason why it can't be both. The only way to know for
               | sure is to ask the author of the recipe that you're
               | reading. :)
        
               | yunohn wrote:
               | When is the last time you heard of online recipe blogs
               | enforcing copyright claims on other blogspam? Ridiculous.
               | 
               | The real reason is simple, people who write recipes
               | aren't robots - they're expressing their stories and
               | emotions, while explaining how to make food that's dear
               | to them..
        
               | fsckboy wrote:
               | > _people who write recipes aren't robots - they're
               | expressing their stories and emotions, while explaining
               | how to make food that's dear to them.._
               | 
               | the people who write recipes aren't robots, they're
               | narcissists and various forms of insecure and seek
               | validation in the form of attention and adulation from
               | others. That's not a bad thing, it's all too human and we
               | should embrace, not stigmatize, the needy, but if all you
               | want is a recipe rather than to be an acolyte it can seem
               | like a big ask.
               | 
               | You enjoyed time with your grandparents, and you remember
               | it? Welcome the club! and I remember family as much more
               | complicated than simply being all fun, and I feel like
               | you might be Norman Rockwelling a bit.
        
               | yunohn wrote:
               | > they're narcissists and various forms of insecure and
               | seek validation in the form of attention and adulation
               | from others
               | 
               | This is incredibly insensitive and judgemental. Not sure
               | what I expected from HN, I guess...
               | 
               | Why are these "narcissistic" people obliged to provide
               | you with formulaic recipes for free? If the cost is
               | perusing over their feel-good story, I feel it's a fair
               | trade-off.
        
               | afiori wrote:
               | The theory that recipes are written to make you scroll at
               | least a full screen to show more ads seems much more
               | plausible
        
               | fsckboy wrote:
               | yes, but you're cart before horse. The structure of paper
               | newspaper and magazine recipe articles and "prose
               | remembrance" cookbooks is the same. Tweaking that model
               | for online use, very plausible, but it's also the way
               | these "stories" are told anyway.
        
             | rendaw wrote:
             | For recipes, it also signals effort and provides a hint
             | about quality. There's a lot of low effort, broken, "would
             | anyone enjoy eating this?" recipes out there dumped on
             | recipe sites. A few pages of text says that the author
             | thought the recipe was at least worth that amount of
             | effort, and usually confirms that the author thinks the
             | recipe is at least as good as X other recipes, etc.
        
             | toyg wrote:
             | It's not about recipes and tech blogs. It's about how
             | American journalists have been taught to write, ever since
             | the days of Capote and Hemingway. Everything else flew from
             | there.
             | 
             | As an European, this is painfully evident every time I read
             | something from a US journalist: I have to fast-forward
             | through several paragraphs of useless "human angle" before
             | I can get to the actual meat of the article.
             | 
             | Unfortunately the rot is spreading further and further
             | every year.
        
       | throwaway81523 wrote:
       | There's a similar thing in tkinter but I guess users discover it
       | faster, since the failure if you don't save the reference shows
       | up fairly quickly.
        
       | Lammy wrote:
       | I experienced a heisenbug exactly like this in Ruby when trying
       | to `while case Ractor::receive`:
       | https://github.com/okeeblow/DistorteD/blob/dd2a99285072982d3...
        
       | sidlls wrote:
       | A better title would be "Bug lurks in incorrect usage of async
       | Python".
       | 
       | The library documentation clearly calls this out, and incorrect
       | implementations, while buggy, do not mean that async Python is
       | itself buggy.
        
       | perlgeek wrote:
       | Thank you! I just did a quick `git grep` in a work code base and
       | found one clear instance of this bug, and two more locations
       | where I'm not 100% certain whether references are kept around
       | long enough. Made a note to open a bug on Monday :-)
       | 
       | Another surprise in python's base library: I knew that re.search
       | searches for a regex match in a string, so I thought that
       | re.match would match the whole string. I was wrong, re.match only
       | anchors the regex at the start, not the end. re.fullmatch anchors
       | it at both sides.
       | 
       | I felt very stupid when I found out; I started at my current work
       | as a Perl developer and learned Python for a new job; but there
       | are two more Python developers (with previous Python experience
       | outside this company) on the same project, and none of them
       | noticed the mistakes I made based on this misunderstanding.
        
       | zzzeek wrote:
       | I think asyncio is kind of neat for what it's good at, but
       | beginner programmers who have never wrote code before are going
       | directly to using Python asyncio (i know this because they are
       | telling me so when they post sqlalchemy discussions). This is
       | just wrong.
        
       | slewis wrote:
       | The response here makes me think most commenters don't have
       | experience with this particular footgun.
       | 
       | To clarify: Python can gc your task before it starts, or during
       | its execution if you don't hold a ref to it yourself.
       | 
       | I can't think of any scenario in which you'd want that behavior,
       | and it is very surprising as a user.
       | 
       | Python should hold a ref to tasks until they are complete to
       | prevent this. This also "feels right", in that if I submit a task
       | to a loop, I'd think the loop would have a ref to the task!
       | 
       | It'd be interesting to dig up the internal dev discussions to see
       | why the "fix this" camp hasn't won.
        
         | kevincox wrote:
         | I can see this behaviour being useful if you are no longer
         | interested in the result of a "pure" task. For example imagine
         | fetching some data via HTTP. If you no longer need to response
         | canceling the request could make sense.
         | 
         | But I agree that this is unexpected and most code probably
         | isn't ready for being cancelled at random points. (Although I
         | guess in Python your code should be exception safe which is
         | often similar)
        
         | BerislavLopac wrote:
         | A possible solution might be a built-in dunder flag to
         | explicitly tell the interpreter _not_ to get rid of a specific
         | object. Something like __keep_alive__ or similar.
        
         | hgomersall wrote:
         | That would make tidying up rogue tasks impossible. Of course we
         | all like to think we do cancellation perfectly, but it's nice
         | to know that the task scheduler has your back.
         | 
         | Edit: I don't quite understand why a user would expect a task
         | to remain live _after_ the last reference to it has been
         | dropped...
        
       | syngrog66 wrote:
       | Java had a similar but inverse problem in early versions. A
       | counter-intuitive behavior that bit people and caused leaks.
       | 
       | If you instantiated a Thread, and then start() was never called,
       | that thread object would leak. And thus potentially an entire
       | graph of objects, via the references chains beneath it.
       | 
       | Obviously a thread that is never started seems pointless by
       | design. But it _could_ happen easily if, for an example, an error
       | happened or an exception was thrown at some point between the
       | instantiation line and the call of start().
       | 
       | The root cause was because Sun's programmers had made the early
       | implementations of Thread get added to a ThreadGroup by default,
       | under the hood. What would happen is that ThreadGroup stayed
       | alive/reachable and thus it kept your app's thread object
       | reachable too, and thus the GC would never clean it up. It was
       | never eligible.
       | 
       | It ended up being the cause of a few weird leaks we saw in
       | production.
       | 
       | IIRC in Java 1.4 or 1.5 Sun fixed it by ensuring the thread got
       | cleaned up in those cases.
        
       | qwertox wrote:
       | I'm using a lot of `asyncio.get_event_loop().create_task(...)`
       | calls without assigning the task to a variable, but the docs [0]
       | don't mention anything regarding this method on the loop object.
       | Can I assume that I'm safe?
       | 
       | [0] https://docs.python.org/3/library/asyncio-
       | eventloop.html?hig...
        
         | quietbritishjim wrote:
         | I'm pretty sure that's equivalent to asyncio.create_task(), it
         | just gives you the opportunity to specify a different loop if
         | needed. I think the docs are just less explicit because they're
         | aimed more at power users (since most people don't deal with
         | multiple event loops).
        
       | samwillis wrote:
       | This is one of many reasons I'm sceptical of the current trend in
       | Python to "async all the things". The nuance to how it operates
       | is often opaque to the developer, particularly those less
       | experienced.
       | 
       | GUI toolkits (like Textual) however are a really good use case
       | for Asyncio. Human interaction with a program is inherently
       | asynchronous, using async/await so that you can more cleanly
       | specify your control flow is so much better than complicated
       | callbacks. Using async/await in front end JS code for example is
       | a delight.
       | 
       | Where I'm particularly unconvinced of their use is in server side
       | view and api end point processing. The majority of the time you
       | have maybe a couple of IO opps that depend on each other. There
       | is often little than can be parallelised (within a request) and
       | so there are few performance gains to be a made. Traditional
       | synchronous imperative code run with a multithreaded server is
       | proven, scalable and much easier to debug.
       | 
       | There are always places where it's useful though, things such as
       | long running requests (websockets, long polling), or those very
       | rare occurrences where you do have many easily parallelizable IO
       | opps within one short request.
        
         | heavyset_go wrote:
         | > _Where I 'm particularly unconvinced of their use is in
         | server side view and api end point processing. The majority of
         | the time you have maybe a couple of IO opps that depend on each
         | other. There is often little than can be parallelised (within a
         | request) and so there are few performance gains to be a made.
         | Traditional synchronous imperative code run with a
         | multithreaded server is proven, scalable and much easier to
         | debug. Traditional synchronous imperative code run with a
         | multithreaded server is proven, scalable and much easier to
         | debug._
         | 
         | Python doesn't have multithreading that scales or supports real
         | parallelism. asyncio has very measurable performance benefits
         | for exactly that use case you've mentioned versus threaded
         | servers.
        
           | zzzeek wrote:
           | Sorry that's not accurate. Asyncio and threading offer the
           | same variety of "parallelism" , which is that both can wait
           | on multiple io streams at once (the gil is released waiting
           | on io). Neither offer CPU parallelism, unless lots of your
           | CPU work is in native extensions that release the gil. In
           | that unusual case, threading would offer parallelism where
           | asyncio wouldn't.
           | 
           | Asyncio's single advantage is you can wait on _lots_ of io
           | streams, like many thousands, very cheaply without having to
           | roll non blocking IO queueing code directly.
        
             | heavyset_go wrote:
             | I didn't say that asyncio offered parallelism, I'm pointing
             | out that normal assumptions about multithreading you'd make
             | with other languages don't always apply to Python. You'd
             | typically assume that threads offer parallelism, a property
             | you might choose to use them for over something like
             | single-threaded asyncio.
             | 
             | I've found that for even IO bound workloads, the amount of
             | throughput plateaus when using a relatively small amount of
             | threads despite the GIL being released on IO.
        
               | zzzeek wrote:
               | sorry for misreading that from your post! my own benching
               | with threads vs. asyncio has never found any performance
               | difference between the two approaches (asyncio slightly
               | slower). if you need very wide throughput, then yes
               | asyncio is better. otherwise, it's very difficult to
               | create equitable comparisons between threaded and asyncio
               | code.
        
               | heavyset_go wrote:
               | Haha, it's cool. I once had a problem where I needed to
               | scrape a bunch of sites at once, and ended up using it as
               | a benchmark for threads vs asyncio. The overhead with
               | threads was high even when using a handful of threads,
               | and quickly hit a plateau after scaling to 4-8 threads on
               | a 16 core machine. I was a big fan of the
               | concurrent.futures executor model and generally thought
               | it was good performance anyway, but figured I'd try to
               | implement it in asyncio, and rate limited both for a
               | fairer comparison. It was so shockingly fast that it was
               | the last project I reached for threads again with on
               | Python. I can't say that it was an equitable comparison,
               | but it certainly left an impression on me.
        
         | Topgamer7 wrote:
         | These days with graphql, or complex microservices
         | architectures, you could have multiple hops to fulfil l the
         | original request.
         | 
         | Flask sync will hold that thread hostage until the request is
         | done. Where async with properly used async libs will allow
         | other requests to process.
         | 
         | We often have medium sized reports take seconds. That is a lot
         | of time to wait. And would just end up bloating your service
         | scaling to handle more connections.
         | 
         | Any service with decently long lived network requests will
         | benefit from event loop handled scheduling.
        
           | jsyolo wrote:
           | I don't think he was referring to python when he mentioned
           | server-side, but to real multi-threaded runtimes.
        
         | traverseda wrote:
         | >Where I'm particularly unconvinced of their use is in server
         | side view and api end point processing.
         | 
         | Sure, performance isn't going to get better, but for websockets
         | and server sent events the occasional long-lived async task can
         | be great. Especially when you need to poll something, or check
         | in on a subprocess.
        
         | nbadg wrote:
         | The thing is, there's a lot more nuance to it than this.
         | Async/await is part of the language syntax in python, but
         | asyncio is only one particular implementation of an event loop
         | framework to power it. But really what async/await provides is
         | a general-purpose cooperative multitasking syntax. This allows
         | other libraries to implement their own event loop frameworks,
         | each with their own different semantics and considerations (the
         | two best-known alternatives being Curio and Trio). At a
         | language level, there's nothing even forcing you to use
         | async/await for ascync IO -- you could, if you really wanted,
         | probably write a library that used it to start threads and
         | await their completion.
         | 
         | So you have, from highest-level to lowest-level: application
         | code, async/await language syntax, the event loop framework,
         | and then the implementation of the event loop itself. The OP
         | article concerns a peculiar implementation detail in the lowest
         | level that makes it very easy to write bugs at the highest
         | level.
         | 
         | But that means that even if you do "async all the things",
         | you'll only encounter this situation if you write your
         | application code in a particular way. It just so happens that
         | "in a particular way" is, in this case, the overwhelming
         | majority of how people write it, which is, of course, why the
         | OP article is relevant.
        
           | heavyset_go wrote:
           | > _The OP article concerns a peculiar implementation detail
           | in the lowest level that makes it very easy to write bugs at
           | the highest level._
           | 
           | Are other async implementations using the asyncio.Task
           | abstraction? I haven't looked into it, but I assumed that
           | asyncio.Task was tied to the asyncio implementation and event
           | loop.
        
             | nbadg wrote:
             | asyncio.Task is part of the asyncio event loop framework.
             | So any event loop implementation that conforms to that
             | framework will have to have one (including the default
             | event loop implementation that ships with asyncio). So for
             | example, uvloop, which is an alternative event loop
             | implementation that works with the asyncio event loop
             | framework, also uses asyncio Tasks.
             | 
             | Other event loop frameworks can do whatever they want, and
             | presumably, wouldn't be importing from asyncio. Whether
             | they have a similar abstraction is completely up to the
             | framework itself. Trio, for example, doesn't have a concept
             | of a task _object_ at all, because it enforces a strict
             | tree structure for tasks.
        
           | LtWorf wrote:
           | > This allows other libraries to implement their own event
           | loop frameworks
           | 
           | At work someone replaced the default library with another
           | faster implementation.
           | 
           | Then the unix socket listener task was not working.
           | 
           | A few hours of git bisect later, I found out the offending
           | commit was the 1 line switching the event loop. Seems the
           | fast implementation didn't implement unix sockets and just
           | had "pass" in the function.
        
         | pdonis wrote:
         | _> GUI toolkits (like Textual) however are a really good use
         | case for Asyncio._
         | 
         | Only if the GUI toolkit is explicitly written to be asyncio-
         | aware and use asyncio's event loop. Textual appears to be
         | written specifically to do that.
         | 
         | However, other GUI toolkits that I'm aware of that have Python
         | bindings aren't written that way. Qt, for example, uses its own
         | event loop, and if you want anything other than a GUI event to
         | be fed into Qt's event loop so your event-driven code can
         | process it, you have to do that by hand and make sure it works.
         | There is no point in even trying to use another event loop,
         | such as Python's asyncio event loop, since that loop will never
         | run while Qt's event loop is running.
        
           | harpiaharpyja wrote:
           | There's a qasync library that I've used to bridge asyncio and
           | Qt, it works pretty well. But yes, you need some extra
           | support.
        
         | samsquire wrote:
         | I am a huge fan of parallel and async code. I spend a lot of
         | time researching it and trying to design software that is
         | easily parallelisable.
         | 
         | Many GUIs use the event/message pump pattern, such as Windows
         | 32 API. Qt does something with its event loop (QEventLoop)
         | 
         | Threads are a rather low level instrument to get background
         | tasks going because the interface between the main thread and
         | the threads is rather omitted.
         | 
         | In Java you could use a ConcurrentLinkedQueue. And in Python
         | you can use JoinableQueue.
         | 
         | I am heavily interested in this space because I want to write
         | understandable software that anybody can pick up and work with.
         | I worked on a JMS log viewer that used threads but would crash
         | with ConcurrentModificationException due to not being thread
         | safe. I changed it to be thread safe but its performance
         | dropped through the floor. In my learnings since then I should
         | hast sharded each JMS connection topic to its own thread or
         | multiplexed multiple JMS topics per thread and loop over them.
         | The main thread can interrogate the thread with a lock, that
         | should be faster than every thread trying to acquire the lock.
         | It would be driven by the main thread but the work is done in
         | the background. The threads can keep the fetched messages in
         | memory until the main thread is ready for them.
         | 
         | I think with the right abstraction, thread safety can be
         | achieved and concurrency shouldn't be something to be afraid
         | of. It is very difficult and challenging working at the low
         | levels of concurrency such as a concurrent browser engine.
         | (I've not done that though.)
         | 
         | This is why languages such as Pony lang, Inko, Cyber and
         | Erlang, Elixir are so promising. We can build high performance
         | systems that parallelise.
         | 
         | Writing an async/await pipeline that looks synchronous is far
         | easier to understand and maintain than nested callbacks. So I
         | can see where async is useful. I just hope we can design async
         | software to be simpler to maintain and extend.
        
         | whoopdeepoo wrote:
         | I don't write any colored function code in python, I'd much
         | rather work with process/thread pools
        
           | hgomersall wrote:
           | I really don't get the whole coloured function thing. How's
           | it not just the function signature? You might as well claim a
           | new function argument makes a new colour. Granted, all of my
           | use of async is in Rust in which the compiler picks this
           | stuff up, so maybe in python there are other concerns I'm
           | missing.
        
             | harpiaharpyja wrote:
             | Honestly, I think the whole colored function thing is BS.
             | I've never run into an issue with it.
        
           | Animats wrote:
           | Me too, but threading is botched in Python. Not just the
           | Global Interpreter Lock. Some Python packages are not thread-
           | safe, and it's not documented which ones are not. Years ago I
           | discovered that CPickle was not thread safe, and that wasn't
           | considered a problem.
        
             | jsyolo wrote:
             | Since the GIL is there and it restricts multithreaded
             | issues to happen, why would python packages be thread-safe?
        
               | Jorge1o1 wrote:
               | You can still have thread safety issues with the GIL in
               | place, because globals and other data is shared between
               | threads.
               | 
               | For example, you can put a dictionary at the module
               | level, thread A can set a key in that dictionary like
               | "name", thread B can overwrite it, and then thread A
               | comes back, does dct["name"] and gets an unexpected
               | answer.
               | 
               | This is a relatively easy mistake to make, a lot of
               | python code has module level variables.
        
         | michael_j_x wrote:
         | I am not sure I agree that the GUI is a good use case for
         | async. A human interaction with the program must almost always
         | pre-empt whatever the program was running, so I can not see how
         | a cooperative multi-threading runtime like async Python can
         | work in such a scenario.
        
           | harpiaharpyja wrote:
           | Well it does work so "it can't work" isn't a very substantial
           | criticism.
           | 
           | Is there some nuance or detail that you meant to include?
        
       | kodablah wrote:
       | It is for this reason in Temporal Python[0], where we wrote a
       | custom durable asyncio event loop, that we maintain strong
       | references to tasks that are created in workflows. This wouldn't
       | be hard for other event loop implementations to do too.
       | 
       | 0 - https://github.com/temporalio/sdk-python
        
         | make3 wrote:
         | he never said it was hard, his point is that it's unintuitive &
         | a lot of people don't know or don't remember
        
           | kodablah wrote:
           | I mean the default asyncio event loop can be
           | replaced/extended where you won't have to know/remember on
           | each create_task. But yes, it is an unintuitive default.
        
       | NelsonMinar wrote:
       | Does anyone understand why the event loop only keeps weak
       | references to tasks? It'd seem wise to do something to stop it
       | from being garbage collected while running, maybe also while
       | waiting to run.
        
         | coopsmoss wrote:
         | I agree, I think this is very unpythonic behavior
        
         | masklinn wrote:
         | Only guess I'd have is to protect the system against infinite-
         | loop tasks, but I don't remember any other runtime caring and
         | an a task which never terminates seems easier to diagnose than
         | one which disappears on you.
        
         | kortex wrote:
         | Because it's almost always the case that the consumer is going
         | to keep a reference to the task in some way, so that is the
         | logical choice for the "primary owner" of the task. Python
         | doesn't have ownership per se like rust, but if you keep more
         | than one hard reference to an object around, it'll prevent
         | collection, so in cases such as this it makes sense to
         | designate one primary owner and have all other references be
         | weakref.
        
           | skitter wrote:
           | > if you keep more than one hard reference to an object
           | around, it'll prevent collection
           | 
           | Which is the behavior the parent comment asks for.
        
             | kevin_thibedeau wrote:
             | That creates a new problem that you have to remember to
             | kill unwanted threads.
        
               | [deleted]
        
           | JonChesterfield wrote:
           | Python's reference counted - if the event loop holds a
           | reference until the task has run, then drops it, then
           | everything behaves sanely. That's not a cycle. It just means
           | the task that was scheduled will execute, which seems like
           | the right default.
        
       | anthomtb wrote:
       | Well, looks like I know what I am doing first thing on Monday. I
       | converted a bunch of code to asyncio a while back. I have yet to
       | run into any heisenbug in that code and want to keep it that way.
        
       | cpburns2009 wrote:
       | I've been working on a PySide6 application recently using
       | asyncio. I read the docs but totally overlooked the requirement
       | to hold references to tasks created with `create_task()`.
        
       | dehrmann wrote:
       | Eww. What's especially nasty is this is the opposite behavior of
       | threads.
        
       | aeturnum wrote:
       | I really think this writer doth protest too much.
       | 
       | Yes, the base async interface is confusing and overly complex.
       | It's a downside! As they note lots of people have stepped in to
       | provide better helpers (like TaskGroups) - but these are the docs
       | for the base library!
       | 
       | > _But who reads all the docs? And who has perfect recall if they
       | do?_
       | 
       | Everyone reads the docs? That is why you don't need perfect
       | recall because you can read them whenever you want.
       | 
       | Python has lots of confusing corner cases ("" is truthy, you need
       | to remember to call copy [or maybe deepcopy!] sometimes, all the
       | other situations where you confuse weak v.s. strong references).
       | They cause really common bugs. It's just a hazard of the language
       | in general and the choices it makes (much like tasks being
       | objects is a hazard). I do understand why people think they can
       | throw away task references (based on other languages) - but this
       | is Python! The garbage collector exists and you gotta check if
       | you own the object or something else does.
       | 
       | Edit: this feels like an experienced Python developer, who has
       | already internalized all the older, non-async Python weirdness,
       | being taken aback by weirdness they didn't expect. Like, I feel
       | you, it does suck - but it's not a bug that values you don't
       | retain may get garbage collected.
        
         | No1 wrote:
         | He didn't even have to read "all the docs" - just the ones that
         | pertain the the function that he is using. And then not ignore
         | the section marked "Important" _and_ the highlighted  "Note".
        
           | richbell wrote:
           | What if he read the docs for that function prior to the
           | "important" note being added?
        
         | Karunamon wrote:
         | > _Everyone reads the docs?_
         | 
         | The author goes on to say they found this pattern lurking in
         | various projects on github. So, no. The problem is that this
         | behavior is subtle, not intuitive, and unless you are reading
         | the actual documentation top to bottom (and not just the
         | function signature and first paragraph from the pop up in your
         | IDE) you will likely get bitten by this.
         | 
         | What is the point of your comment? The author _shouldn 't_ have
         | called out the upturned rake in the darkened shed?
        
           | rollcat wrote:
           | > The author goes on to say they found this pattern lurking
           | in various projects on github.
           | 
           | I'd call it an anti-pattern. If you spawn a process/thread,
           | and never wait/join it, it means you don't actually care what
           | it does, if it crashes, etc. I don't see a problem with
           | Python's behavior here.
        
             | mannykannot wrote:
             | The chances are that most people doing this are introducing
             | some nondeterminism that they did not expect, and will have
             | a hard time dealing with it when it bites them in the ass.
             | 
             | What's more to the point is that _I_ am going to have a
             | hard time when it leads to a serious outage or security
             | violation at some major corporation that has become too
             | pervasive in its reach for me to avoid its influence. No
             | amount of schadenfreude is going to compensate for that.
        
             | hn_throwaway_99 wrote:
             | Seriously?? I think the vast majority of developers would
             | find it very surprising that the Python runtime would GC a
             | task _in the middle of execution_. I would expect that the
             | runtime would by default do what the example in the doc
             | says, which is keep a strong reference to the Task object
             | until it finished execution.
        
           | aeturnum wrote:
           | I wouldn't say _shouldn 't_ - they are free to do what they
           | want. But this is a blog post about something that can trip
           | you up that the docs highlight - which the author calls a
           | "heisenbug". The author doesn't even have a suggestion for
           | the docs, which already calls out the problem they
           | encountered, they just note that there are helpers for this
           | problem (which is true).
           | 
           | The point of my comment is that subtle, non intuitive things
           | like this are all over Python and, while this one is
           | _particularly bad_ , this blog post makes it seem like more
           | of an aberration than it is.
        
         | IshKebab wrote:
         | > Everyone reads the docs?
         | 
         | Wow I've heard people say that everyone _should_ read all of
         | the docs (which isn 't really true) but I've never heard anyone
         | claim that everyone _does_ read all of the docs! Wild.
        
         | raverbashing wrote:
         | > "" is truthy
         | 
         | Humm, no? Unless you mean ("",)                   >>> not ""
         | True
        
           | aeturnum wrote:
           | Oh, sorry, you are right - "" is false-y, even though it's a
           | valid empty value. So it's hard to tell the difference
           | between a value not being filled and a value being filled
           | with an empty value.
           | 
           | ex:                 answers = {}       answers["I exist"] =
           | ""       if answers["I exist"]:           print("a")
           | 
           | does not print.
        
             | fbdab103 wrote:
             | I guess I am too deeply in the Python ecosystem to see a
             | problem here. Unless you want to check for the existence of
             | "I exist"? In which case, the Python Way would be
             | answers = {}       answers["I exist"] = ""       if "I
             | exist" in answers:           print("a")
        
               | pacaro wrote:
               | Maybe                 ...       if answers.get('I
               | exist'):         print('a')
               | 
               | Which is why you should always explicitly check for
               | _None_ if that is your intent.
        
               | aeturnum wrote:
               | It's not a problem? The async interface isn't a problem
               | either. It's just a thing you have to remember about
               | python: "most input is truthy except for the input that
               | isn't"
               | 
               | "Most of the time you don't disrupt your program by not
               | keeping the returned reference in scope except for when
               | you do"
               | 
               | It's just a thing that trips people up.
        
               | tomn wrote:
               | The alternative here (which would make your example work)
               | is that dictionary lookups return some falsy sentinel
               | value when the key does not exist, just like in
               | javascript. In javascript, this has made a lot of people
               | very angry and been widely regarded as a bad move.
               | 
               | Those values tend to propagate out and make a mess,
               | sneaking into your stored data or causing errors in
               | unexpected places -- fixing this feels like one of the
               | main advantages of using things like typescript, and it's
               | just not an issue in python.
               | 
               | It's not a problem, because the alternative is way worse.
               | It's just a different language design than the one you're
               | used to.
               | 
               | You could argue about whether or not truthiness makes
               | sense as a concept (personally I think not), but the way
               | it's defined in python is quite coherent and useful in
               | practice.
        
               | heavyset_go wrote:
               | > _The alternative here (which would make your example
               | work) is that dictionary lookups return some falsy
               | sentinel value when the key does not exist, just like in
               | javascript. In javascript, this has made a lot of people
               | very angry and been widely regarded as a bad move._
               | 
               | You get that behavior by using dict.get(): answers.get("I
               | exist") returns None, which is falsy.
        
               | tomn wrote:
               | Right, sometimes you want that. I suppose my point was
               | that the definition of truthiness works well with the
               | default, most obvious way of indexing a dictionary, which
               | was the original complaint.
               | 
               | Perhaps I was a bit harsh -- that exposes an issue which
               | does trip people up, where they use `if x:` when you mean
               | `if x is not None:`.
               | 
               | Saying that, I think it's defensible in the same way. The
               | fact that you can write `if x:` (where x is not a bool)
               | tells you that the language has a concept of truthiness,
               | and so maybe you should have a think about what that
               | actually means.
        
               | dwattttt wrote:
               | > It's just a thing you have to remember ...
               | 
               | The more of these things there are, the more brainpower
               | you devote to remembering the right way to do things; if
               | you don't you introduce bugs, a subtle, painful one here.
        
               | heavyset_go wrote:
               | "Empty containers are falsy" is a Python fundamental,
               | this isn't a subtle bug, but an obvious one.
        
               | tomp wrote:
               | > "most input is truthy except for the input that isn't"
               | 
               | Can you think of any other value that has len(x) == 0 but
               | is truthy?
               | 
               | It's quite simple. Empty collections and zeros are false,
               | almost everything else is true.
               | 
               | The real head scratcher is that midnight is false.
        
               | fbdab103 wrote:
               | Truthy is a Pythonic core principle of the language. It
               | is not an edge case phenomenon in the language which I
               | would expect a regular practitioner to confuse.
               | 
               | https://docs.python.org/3/library/stdtypes.html#truth-
               | value-...
        
               | aeturnum wrote:
               | I mean, I've seen bugs around that in code I've worked on
               | and I've created bugs where it's a factor.
               | 
               | Weakrefs are also a core part of the language:
               | https://docs.python.org/3/library/weakref.html . You
               | can't use python without using them.
        
               | fiddlerwoaroof wrote:
               | What I learned when I wrote Python professionally was
               | "never rely on truthiness" explicitly writing out a
               | boolean expression that does what you want is more
               | explicit ("explicit is better than implicit", PEP 8) and
               | prevents a whole class of bugs down the line.
        
               | nemetroid wrote:
               | PEP 8, which you mention, explicitly recommends relying
               | on truthiness:
               | 
               | > For sequences, (strings, lists, tuples), use the fact
               | that empty sequences are false:                 #
               | Correct:       if not seq:       if seq:            #
               | Wrong:       if len(seq):       if not len(seq):
        
               | AeroNotix wrote:
               | PEP8 is touted a lot as if it is a perfectly correct tome
               | of ... correctness. I've worked in Python long enough to
               | know that it both doesn't cover everything and the advice
               | is sometimes actively bad.
        
               | raverbashing wrote:
               | Amen
               | 
               | I mean, I'm not blaming PEP8 per se (A Foolish
               | Consistency is the Hobgoblin of Little Minds) but it has
               | a tendency to be taken as gospel by a lot of people
               | 
               | Funny enough, those who push for more strict adherence
               | are also the ones that neglect other aspects (speed, alg.
               | complexity, etc) especially _readability_ (and no, PEP8
               | compliant code is not necessarily the most readable)
        
             | heavyset_go wrote:
             | > _if answers[ "I exist"]:_                   if "I exist"
             | in answers:              ...
        
             | wizzwizz4 wrote:
             | > _So it 's hard to tell the difference between a value not
             | being filled and a value being filled with an empty value._
             | >>> answers = {}       >>> if answers["I don't exist"]:
             | ...     print("a")            Traceback (most recent call
             | last):         File "<pyshell#3>", line 1, in <module>
             | if answers["I don't exist"]:       KeyError: "I don't
             | exist"
             | 
             | The method you're trying to use doesn't work _anyway_ : it
             | doesn't matter that it's confusing. You'd have the same
             | problem with the value False.
        
             | hn_throwaway_99 wrote:
             | I mean, that's the fundamental reason it's called false _y_
             | , that is, Python does automatic type coercion if it is
             | evaluating things in a boolean context. FWIW this is the
             | exact same behavior in javascript.
        
         | Etheryte wrote:
         | I think you may be too bold with the assumption here,
         | personally I would wager that the majority of people who write
         | Python don't even know Python has official docs outside of a
         | site called Stack Overflow.
        
         | davesque wrote:
         | How is "" truthy? I just double checked and `bool("")` is
         | `False` as I expected (in Python 3.9 at least).
        
         | leni536 wrote:
         | Considering how many times I need to add site:python.org to my
         | python search queries to actually get to the docs, I assume
         | that a surprisingly low number of python developers actually
         | read the docs.
        
           | 0x008 wrote:
           | If you use Druck duck go you can prefix search with "!py3"
        
         | brundolf wrote:
         | In 15 years of programming I've never read the docs for
         | _anything_ front-to-back. I look at the docs when I have a
         | specific question
         | 
         | It's crazy to suggest something this surprising wouldn't be a
         | problem just because it's technically in the docs
        
           | sidlls wrote:
           | This is unfortunately all too common. I am often dinged by my
           | manager about my throughput when investigating the
           | integration of new libraries because I read the docs, front-
           | to-back, as part of my research and that takes time.
        
             | brundolf wrote:
             | I don't think it's really appropriate or feasible or even
             | helpful for most things. At least for myself, if I read
             | through the entirety of a language's documentation I
             | wouldn't remember half of it, and I probably wouldn't need
             | everything I remembered.
             | 
             | Language and library designs should optimize for least-
             | surprise.
        
               | sidlls wrote:
               | Most libraries are quite poorly documented (to my taste,
               | anyhow). It is certainly worth spending the time reading
               | at the very least documentation pertinent to the
               | components of the library intended for use.
        
         | iforgotpassword wrote:
         | > Everyone reads the docs?
         | 
         | For Python? The language where everyone just cobbles together
         | random code from the internet and other repos? I can totally
         | see how this mistake happens left and right. The bar of entry
         | for this language is way too low to assume only rigorous senior
         | devs use it.
        
       | bandyaboot wrote:
       | He doesn't really get into what makes this a Heisenbug, only that
       | it's indeterminate in nature. Would attaching a debugger/stepping
       | through the code make it less likely that your task would get
       | garbage collected out from under you?
        
         | Izkata wrote:
         | You're probably going to need a reference to the task in order
         | to inspect it in the debugger. Creating that reference prevents
         | the bug.
        
         | foobarbecue wrote:
         | Yeah, he seems to be re-defining the term to mean "a bug that
         | occurs occasionally depending on system state" as opposed to "a
         | bug that changes behavior when you observe it closely e.g. in a
         | debugger."
        
           | macintux wrote:
           | The first is a common way of using the term Heisenbug. I
           | first heard it used that way 10 years ago when discussing
           | Erlang's error handling model.
        
             | foobarbecue wrote:
             | TIL. I guess I assumed it would hew more closely to the
             | Uncertainty Principle.
             | 
             | Edit: actually, come to think of it, I first heard of it in
             | about 2006 from Jamie Brandon and at the time assumed it
             | was something he'd made up. For a second there I forgot
             | that 2006 is more than 10 years ago! (It was a python bug
             | that went away when run in a debugger.)
        
         | throwaway81523 wrote:
         | CPython does most of its memory management by reference
         | counting, which fails to reclaim circular structure. So to make
         | sure it gets everything, it occasionally runs a conventional
         | tracing GC. If the GC happens to run just after you create that
         | async task, the task itself can get collected, it sounds like.
         | It's good to know about this and is (my own editorializing) yet
         | another reason Python3 should have used Erlang-style
         | concurrency instead of this async stuff.
        
         | remram wrote:
         | It's actually very not Heisenbug, because if you enable
         | asyncio's debug mode [1], it will tell you what's going on.
         | 
         | [1]: https://docs.python.org/3/library/asyncio-dev.html#debug-
         | mod...
        
       | epakai wrote:
       | I missed this in a little curses program launcher I wrote.
       | 
       | It looks obvious when he puts a big orange box around it, but in
       | the actual docs it's an unassuming paragraph between two border-
       | wrapped blocks with the only distinguishing feature being the
       | bold "Important".
       | 
       | It should probably be referenced immediately next to the "Return
       | the Task object" sentence.
        
       | No1 wrote:
       | His argument hinges on "I can't be bothered to read the docs on
       | the stuff I'm using." So instead of reading the docs on
       | coroutines and tasks before using them, writes a rant about how
       | it's all wrong because he didn't understand how it works.
       | 
       | On a more fundamental level, why would anyone assume that a
       | coroutine is guaranteed to complete if it is never awaited? There
       | is no reason a scheduler could not be totally lazy and only
       | execute the coroutine once awaited.
       | 
       | At least he bothered to make note of TaskGroups, also clearly
       | shown in his documentation screenshot, immediately above the
       | section marked _Important_ that went ignored, and finishes with
       | "As long as all the tasks you spin up are in TaskGroups, you
       | should be fine." Yep, that's all there was to it.
        
         | ptx wrote:
         | > _There is no reason a scheduler could not be totally lazy and
         | only execute the coroutine once awaited._
         | 
         | Isn't the point of create_task (which is what the article is
         | about) to launch concurrent tasks without immediately awaiting
         | them? The example in the docs [1] wouldn't work (in the stated
         | manner) if the task didn't start until it was awaited.
         | 
         | > _At least he bothered to make note of TaskGroups [...] Yep,
         | that 's all there was to it._
         | 
         | That only works on Python 3.11, which was released just a few
         | months ago. Debian still uses 3.9, for example, so the
         | TaskGroups solution can't be used everywhere yet.
         | 
         | [1] https://docs.python.org/3/library/asyncio-
         | task.html#coroutin...
        
           | No1 wrote:
           | The reason I said "on a more fundamental level" is that I'm
           | not talking specifically about Python and asyncio, but
           | coroutines in general. Even for Python, there are multiple
           | event loop libraries available, they do not all work
           | identically, which is why multiple ones exist. Someone here
           | mentioned Temporal Python which works differently from
           | asyncio, and would have avoided the author's problem. If you
           | don't know how the scheduler works, you can't assume that a
           | coroutine is guaranteed to complete just because you yoloed
           | it into the scheduler, no matter how convenient that might be
           | for you.
           | 
           | Yes, TaskGroups are a recent addition. If you can't use
           | Python 3.11 for whatever reason, there is also the clearly
           | written code sample at the bottom of the create_task
           | documentation, which the author did not bother to mention.
           | Probably didn't make it that far.
        
         | zackees wrote:
         | [dead]
        
       | [deleted]
        
       | [deleted]
        
       | [deleted]
        
       | [deleted]
        
       | m3047 wrote:
       | Hrmmmm.
       | 
       | > But who reads all the docs?
       | 
       | asyncio.create_task() doesn't exist in 3.6, and I can't find the
       | string "to avoid a task disappearing" in the doc, so I'll go out
       | on a limb: there is no such doc. However I see the reference to
       | weakref.WeakSet.
        
         | Jtsummers wrote:
         | The world didn't end in 2016. Welcome to seven years in the
         | future where this documentation does, in fact, exist:
         | 
         | https://docs.python.org/3/library/asyncio-task.html#asyncio....
        
           | m3047 wrote:
           | Some of us have been writing python since 2.x, and quite
           | unsurprisingly wrote asyncio code at 3.6, and still happily
           | support it. Some of us have even asked on HN about
           | maintaining compatibility backwards and forwards between 3.6
           | and 3.11.
           | 
           | The documentation didn't exist at 3.6, when I wrote the code.
           | I went and checked the source code, and the documentation and
           | reported my findings. Good to know that there's a potential
           | problem, don't you agree? What would you do differently?
        
       | cutler wrote:
       | Maybe grafting async onto a single threaded dynamic language just
       | isn't such a good idea in the first place.
        
         | hn_throwaway_99 wrote:
         | Works absolutely fine in JavaScript, and the language is
         | certainly much better for it than it was before async/await.
        
         | murphy214 wrote:
         | bingo
        
       | postultimate wrote:
       | This one seems quite easy to fix - just have the scheduler check
       | that task objects have a positive refcount before running them,
       | including the first time.
        
         | remram wrote:
         | A positive refcount doesn't mean the object is not dead.
        
           | postultimate wrote:
           | But the lack of a positive refcount means that it is, so this
           | solves the problem the article was complaining about.
        
             | remram wrote:
             | It works unless it doesn't, so it stays a Heisenbug.
        
       | qxmat wrote:
       | Python has a few weird issues like this. The last one I
       | encountered was with a class inheriting Thread, join and the SQL
       | Server ODBC driver on Linux. Fairly sure I hit page faults thanks
       | to a shallow copy on driver allocated string data but didn't have
       | the time to investigate like the hero of this blog post.
        
       | whoopdeepoo wrote:
       | > But who reads all the docs
       | 
       | Why is this so common? Do people seriously not read a
       | language/library documentation? That's the absolute first thing I
       | do when evaluating a technology.
        
         | adamckay wrote:
         | Because people have deadlines and need to get things working.
         | You read enough to figure out how to do what you need to do and
         | then mostly move on.
         | 
         | This function was added in 3.7 with no note on the importance
         | of saving a reference. In 3.9 a note was added "Save a
         | reference to the result of this function, to avoid a task
         | disappearing mid execution." which was then expanded with the
         | explanation of a weak reference in 3.10.
        
         | skitter wrote:
         | It absolutely is common. People see there is a len function
         | that takes one argument, they call len(some_collection), see
         | that it indeed returns the number of items in the collection
         | like they expect and move on. They don't expect len to return a
         | negative number instead on Thursdays, and of course it doesn't
         | because that would be a pretty big footgun. People also see
         | that there is a create_task function that takes a coroutine,
         | they call create_task(some_coroutine), see that the coroutine
         | indeed runs like they expect, and move on. Sure, you're
         | _supposed_ to await the result, but maybe they don 't need the
         | awaited value anymore, only the side effects, and see that it
         | still works.
        
         | throwaway81523 wrote:
         | I had a manager who actually told me not to read docs. I was a
         | bad report and read them anyway.
        
       | winter_blue wrote:
       | This article just makes me feel like Python, while a language
       | with nice-ish syntax, is a language that was poorly hacked and
       | put together with little concern/thought about the real-world
       | implications of poor design decisions like this async design
       | decision (and also dynamic typing - _a terrible thing in any_
       | language).
        
         | crdrost wrote:
         | Most languages have something like this, usually around async.
         | 
         | For instance NodeJS has had a bit of this around promises, and
         | eventually needed to institute the rule "if a promise rejects
         | with an error, anf nobody is around to hear it, we will crash
         | your program on the assumption that you probably needed to
         | clean up some resources but didn't and now they're going to
         | leak. Listen to the error with a handler that does nothing, if
         | we are wrong about that."
        
           | macintux wrote:
           | One of many reasons I like Erlang: _everything_ is async, so
           | you have plenty of tooling /libraries/core language features
           | to support you.
        
         | philwelch wrote:
         | Python 2.7 was a nice little language. Python 3 is a bit of an
         | overwrought monster at this point.
        
         | photochemsyn wrote:
         | 'async footguns' returns 20,000+ hits on Google. Top one
         | happens to be:
         | 
         | https://news.ycombinator.com/item?id=32086973
         | 
         | > "Async seems to be the first big "footgun" of Rust. It's
         | widespread enough that you can't really avoid interacting with
         | it, yet it's bad enough that it makes..."
        
       | deschutes wrote:
       | Fun stuff. Why aren't unfinished tasks gc roots?
        
       | [deleted]
        
       | kyrofa wrote:
       | Great post. Feels like something a linter should catch.
        
       | [deleted]
        
       | dehrmann wrote:
       | Another common async footgun I see is unthrottled gathering, and
       | no throttling mechanism in the standard library. Once you gather
       | an unspecified number of awaitables, bad things start to happen,
       | either with CPU starvation, local IO starvation, or hammering an
       | external service.
       | 
       | What I like about threads is they make dangerous things like this
       | harder, and you have to put more thought into how much concurrent
       | work you want outstanding. They also handle CPU starvation better
       | for things that are latency-sensitive. I've seen degenerate
       | requests tie up the event loop with 500 ms of processing time.
        
         | rednafi wrote:
         | Huh! Unless you're using semaphores, you can also recreate
         | similar situation with threads. Spin up a whole bunch of
         | threads and send all of them towards some shared object or make
         | 100s of requests with them.
         | 
         | There's not much difference between spinning up threads
         | explicitly and creating async task with asyncio.create_task. In
         | either case, you can throttle them with semaphores.
        
           | dehrmann wrote:
           | I don't have a source or affected versions, but semaphores
           | can scale poorly. I vaguely remember each blocked acquire
           | getting checked on every event loop iteration, or something
           | silly like that.
        
       | acjohnson55 wrote:
       | Something linters can help with would think?
        
         | ryanianian wrote:
         | C++ has nodiscard which is super useful for scenarios like this
         | where ownership can be tricky.
        
       | jasonlaster11 wrote:
       | Unfortunately we don't support python yet, but if you ever
       | encounter a similar heisenbug in JS, you can try recording it
       | with Replay.io
        
       | smetj wrote:
       | Start a thread/greenthread/fiber/process/task without holding a
       | reference to at least tie all loose ends at exit? Hmm dunno.
        
         | tgv wrote:
         | You can do that in go. You don't even get a reference to the
         | thread/goroutine.
        
         | nixpulvis wrote:
         | Fire and forget.
        
       | crabbone wrote:
       | In many years since asyncio has been added, I have never used it
       | willingly, outside of the cases where a third-party library
       | required it. There has never been a practical benefit for any of
       | that stuff when compared to select. It always worked poorly and
       | never justified the effort one has to put into writing code that
       | uses the library. The behavior OP describes is just one of the
       | many bad design decisions that are so characteristic of this
       | library.
        
       | 29athrowaway wrote:
       | Reading this was like the first time I read "for/else". "wtf?!"
       | was my first reaction, then I read the documentation.
        
       | pyuser583 wrote:
       | I don't find this behavior odd at all. Dereferencing unassigned
       | values is normal Python garbage collector behavior. Threads are
       | an exception (no pun intended), but they're an exception in lots
       | of ways - just try pickling them.
        
       | samsquire wrote:
       | Thank you for this. This is really useful information.
       | 
       | I recently adapted some garbage collection code to add register
       | scanning.
       | 
       | I can imagine all sorts of subtle bugs where things go away
       | randomly. One problem I have with my multithreaded code is that
       | sometimes a thread crashes and the logs are so long I don't
       | notice. From my perspective the thread is just not doing
       | anything.
       | 
       | Sometimes the absence of behaviour can be really tricky to debug!
        
       | sgt wrote:
       | Is this something go developers also have to be careful with when
       | using goroutines?
        
         | gerad wrote:
         | No. But sometimes goroutines have the opposite problem, where
         | they don't terminate and get cleaned up.
         | 
         | https://betterprogramming.pub/common-goroutine-leaks-that-yo...
        
           | [deleted]
        
           | candiddevmike wrote:
           | Is there an (easy?) test for checking goroutine leaks?
        
             | Snawoot wrote:
             | Yes, it's visible on goroutine profile, provided by built-
             | in profiler pprof. E.g.: https://github.com/mysteriumnetwor
             | k/node/issues/5311#issueco...
        
         | Jtsummers wrote:
         | No. Goroutines don't generate a reference to hold onto, either.
         | They just run until they or the program terminate.
        
         | [deleted]
        
       | makomk wrote:
       | Well, this explains that one really annoying intermittent bug
       | that I was having in some asyncio-based code.
        
       | magicalhippo wrote:
       | Delphi had the opposite bug in its thread pool. The worker
       | threads would dequeue a work item and process it in a loop. The
       | work items were reference counted.
       | 
       | Now, Delphi doesn't have scoped variable declarations like say
       | C++ or C#, so the dequeued work item was stored in a local
       | variable. However, it didn't drop (nil/null) the work item
       | reference before it looped. Thus it would hang on to that
       | reference until the next work item got dequeued or the pool was
       | destroyed.
       | 
       | The result was that if you in a function started a task which
       | captured a local reference (f.ex. using an anonymous function)
       | and then waited for it, that reference could live after your
       | function returned if the pool didn't have anything else to do.
       | Not what most people would expect.
        
       | aardvark179 wrote:
       | The same problem or something similar exists in many languages.
       | Threads are GC roots because the OS knows about them, but this
       | may not be true for lightweight threads or async callbacks.
       | 
       | It is hard to fix because you don't want to introduce references
       | from an old object (such as a list of callbacks) to many new
       | objects as that will introduce GC issues, and many other
       | potential leaks.
        
       | collinvandyck76 wrote:
       | It seems like the library should retain a handle on the task
       | until it completes.
        
         | nerdponx wrote:
         | This is partly the goal of structured concurrency, implemented
         | as Nursery in Trio and TaskGroup in Asyncio.
        
       | jarboot wrote:
       | If I want to create a task that runs even after the function
       | returns, ie "async def f():
       | asyncio.create_task(coro=10_second_coro.run()); return;" is there
       | any way to mitigate this? Function-scoped set of tasks?
        
         | nhumrich wrote:
         | Yes, read the last part of the included documentation and hold
         | onto background tasks.
        
         | jmholla wrote:
         | Your task is implicitly not function-scoped as you want it to
         | survive exiting the function. What your doing here would be
         | better architecturally done with threads. async is not a direct
         | replacement for threading.
         | 
         | But, you could also return the task object to the caller and
         | have them manage it. There's also nothing async about your
         | function, so you don't need the async or to await it.
        
       | mkarliner wrote:
       | Thank you!
        
       | phyzome wrote:
       | That seems very odd for a _default_ behavior. There might be good
       | reasons to allow GCing of a dropped task reference, but it doesn
       | 't seem like that would be the most common case.
        
       | cmstodd wrote:
       | Thanks for posting.
        
       | nixpulvis wrote:
       | Hey, at least it's documented... good developers actually RTFM.
       | 
       | I can't comment on the design of this API, because I don't feel
       | like learning the library, but in some performance critical
       | applications these sorts of contracts aren't all that uncommon.
       | Granted, this is python, I guess it's a bit more suspicious, IDK.
        
         | vbernat wrote:
         | The documentation update is quite recent (Python 3.11). It was
         | added after this ticket: https://bugs.python.org/issue44665
         | (not the first ticket around this problem).
        
       | [deleted]
        
       | osigurdson wrote:
       | A little pedantic but HUP concerns the fundamental limits of
       | simultaneously knowing a particle's position and momentum, not
       | about observation impacting outcomes.
        
       | blacklight wrote:
       | I've used asyncio.create_task forever, and I've admittedly never
       | read its documentation in depth.
       | 
       | However, I've ALWAYS assigned the return value of create_task to
       | some variable.
       | 
       | To me this is just a good programming practice. The OP says
       | "tasks are not like threads - that you can just launch and
       | forget" - no! Even a thread should not simply be launched and
       | forgotten! You always need a reference to it, so when your
       | application is terminated you can join all the threads that are
       | still running and things can exit in a clean way! Same goes for
       | the tasks: when your application exits, it's just a good practice
       | to stop the even loop and cancel any pending tasks.
       | 
       | And, in general, one should always keep in mind the reference
       | count rule in Python (the author incorrectly calls it "garbage
       | collection" btw): if something you created in your function isn't
       | referenced/assigned to anything, then its reference count will be
       | zero, and it will be removed when the function stack unwinds.
       | This is totally expected behaviour to me.
        
       | notatoad wrote:
       | wow. yeah, this absolutely explains a heisenbug that i've been
       | chasing for a while. and i can't count the number of times i've
       | had that exact doc page open on my screen in the last few months,
       | and never bothered to read that block of text that starts with
       | "important"...
       | 
       | thanks
        
       | fsckboy wrote:
       | The oop model (or any "client" model of state) dependent on state
       | outside your code being encapsulated/contained inside an object
       | within your program is always confusing. It's not particular to
       | this library or python.
       | 
       | A gui window handle within your program, or simply an open file
       | handle, if the OS does something to your object, it's hard for
       | you to know about it, and your object continually needs to
       | refresh its state if you are concerned about it. I don't know
       | what is referred to as a "task" in this case, but I don't think
       | the lifetime of the actual task is the issue, it's the lifetime
       | or your object.
       | 
       | It's always the case that if you instantiate an object with a
       | ctor, you can't count on anything about it continuing if the dtor
       | is invoked. The problem is much more general than this API, this
       | library, this language, this use case. Just as you need to
       | structure your C code so malloc and free will always match up
       | spatially and temporally, you need to structure your oop code so
       | ctors and dtors match up sensibly. Otherwise the confusion in
       | your head will spread to your code.
       | 
       | And those who always want the compiler and tools to automatically
       | do as much as possible to free them from the burden, are the ones
       | who are most surprised by Heisenbergs. Computers can (or will try
       | to) do anything, it's up to you to make sure it does what it
       | needs to. Maybe someday AIs will do it better than us, but right
       | now you need to provide the I.
        
       | tbrownaw wrote:
       | So a `Task` object is in fact the actual task, rather than a
       | handle or pointer to the actual task.
        
       | aldenpage wrote:
       | That's extremely insidious. I suppose I never encountered this
       | issue because I almost always call asyncio.gather(*), which makes
       | having a collection of tasks natural.
        
         | kortex wrote:
         | This is good form. It makes top-level control flow easier to
         | follow, and keeps the concurrency scoped.
        
       | BiteCode_dev wrote:
       | And this is why trio got it right, and why I think the task
       | groups (nurseries from trio) can't arrive soon enough in the
       | stdlib.
       | 
       | Because not only you must maintain a reference to any task, but
       | you should also explicitly await it somewhere, using something
       | like asyncio.wait() or asyncio.gather().
       | 
       | Most people don't know this, and it makes asyncio very difficult
       | to use for them.
        
         | postultimate wrote:
         | > task groups (nurseries from trio) can't arrive soon enough in
         | the stdlib
         | 
         | Please, no. Asyncio is horrible, and bodging it to make it less
         | horrible just means we will be forced to live with the
         | remaining horror. Far better to replace it with something that
         | works properly (yes, Trio).
        
       ___________________________________________________________________
       (page generated 2023-02-12 23:02 UTC)