A Heisenbug lurking in async Python
textual.textualize.io
textual.textualize.io
And also it's great information that I - like I'm sure many of you - also never noticed. THANK YOU!
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.
Source: https://copyrightalliance.org/are-recipes-cookbooks-protecte...
if(bounced && hosts_doubleclick_ads) pagerank++;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.
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..
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.
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.
The More You Know...
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.
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)
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...
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.
Is there some nuance or detail that you meant to include?
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.
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.
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.
certainly, doing that kind of thing with asyncio likely has a lower learning curve because you don't need to worry about "pools of workers" so much. concurrent.futures should do that for you also but that would be the first place I don't trust
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.
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.
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.
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.
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.
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.
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.
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.
At face value, yeah, every function in Haskell that accepts an IO monad is now "IO colored", but at the same time, that's a silly way to look at it. It's just extra type information and type bindings flowing as types flow through functions. It's just a bit of a tautology that functions that deal with other functions that need that type need to deal with that type themselves.
Functions that use Maybe/Option/nulls are all "nullable colored". Functions that use or return integers are clearly "integer colored". That's what programming languages do: they try to track how your types flow through functions. "Coloring" is a bad metaphor or at least a useless one, we just call that "types". Admittedly, I think that's why Python and JS users predominantly use the "what color is your function" complaints the most because all of the rest of typing information for them is generally opt-in and easily ignorable/forgotten.
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.
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.
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)
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...
I don't follow the argument wrt tidying up rogue tasks. What does it mean for the task to be "rogue"? If there was some state change that made the task redundant - because it clearly wasn't when it was submitted! - then the code that makes that change, or some other code that observes it, should cancel it. If it isn't cancelled, the fact that nobody is able to observe the value that the task will yield is not sufficient to auto-cancel, as there may still be a dependency on side effects.
And, speaking of tidying up, what if the scheduled task is the one that performs some kind of cleanup?
If you have a scheduled task to clean up, then you need to manage it at whatever level that occurs. You signal to notify completion of clean-up to the top (or whatever) level. It's no different to signalling the other way to notify of shutting down.
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.
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.)
it makes cleaning up after myself a LOT easier.
Let's say I write a task that updates a progress bar as an infinite loop, and let it be gc'ed on program exit, without ever joining it. What's wrong with that design? I can, of course, modify the task to check a flag that indicates program completion, and exit when it's set. But does this extra complexity help the code quality in any way?
Or suppose I spawn a task to warm up some cache (to reduce latency when it's used). It would be nice if it completes before the cache is hit, but surely not at the cost of blocking the main program. I just fire-and-forget that task. If it executes only after the cache was hit, it will realize that, and become a no op. Why would I want to join it at the end? It may not be free (if the cache was never hit, why would I want to warm it up now that the main program is exiting?).
Like what, for example? Well, one very simple yet practical one is that when you break into your program with a debugger, you want to minimize noise - any thread or object that's alive unnecessarily is, at the very minimum, extra noise for you to deal with, and at worst, extra surface for a bug to creep in. Moreover, the liveness of the thread/object could provide you with a vital bit of information that you otherwise wouldn't get. Another one is the fact that it lowers the number of obstacles you'll have in the future if you ever want to do something less common - such as suspending a GC, snapshotting the program state, or any number of less common things. Yet another one is the very fact that following the pattern more broadly helps you and future maintainers avoid pitfalls that arise in similar abstractions, like they did here. I could go on, but all of these concerns are basically a bunch of things whose values mostly lie in the potential future, not the present.
There are many other more practically-minded folks who believe the presence of a GC exempts them from caring about such concerns, and see these as adding extra complexity. If you see it that way, I don't have a compelling rebuttal. But if "complexity" is your criterion, perhaps what I can offer is that you can also view it from the opposite standpoint: following the fork-join pattern (or avoiding circular references, etc.) itself avoids complexities that arise from not doing so [1], such as those in the previous paragraph. It's just that not every form of complexity or cost materializes immediately.
[1] Note that complexity is not just a measure of code size, but also the deviation of its behavior from expectation. You can make code more complex to reason about merely by deleting some lines, and that could include a thread.join() call.
In my first example, I would probably find not joining the thread cleaner than joining (since it would require extra code to rewrite the infinite loop into something joinable, and since the earliest time I can join is at the very end of the program anyway).
In my second example, your arguments are persuasive. It is very likely that there is a place in the program where the cache warming is no longer a good idea (for example once the real traffic started hitting the cache, it's probably too late; in fact warming up at that stage is probably a bug, since it may divert resources from serving the actual user traffic). So yes, in my second example, I now think it's better to either join or cancel the task.
The main cases I can think of where joining might not make sense is when you simply don't have the capability to do so in a reasonable manner, like when the main thread is in third-party code that you have no control over. Otherwise, if I understand the example correctly, you absolutely need to join such a thread - and not merely at program exit, but sometime before the GUI is destroyed.
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.
But that's not what makes performance hard to fix. It's rather the fact that most Python code out in the wild depends on packages written in native code, and the CPython ABI for said packages exposes way too many implementation details. If you ditch ABI compatibility, you can ditch GIL, for starters (see e.g. IronPython and Jython) - but few people are willing to make do without all the affected packages.
In lieu of a formal definition, the maintainers resort to a hodgepodge of user docs, PEPs, mailing lists, and the "reference implementation". Worries about making that "reference implementation" more complex stymied Python's development. There's been flamewars about this topic with the PSF and their apologists.
On that note, Rust encountered a similar problem of bad decision-making by maintainers, who also were opposed to specification. They have ejected those maintainers and replaced them with ones who understand the need for a formal definition of their language.
And if users don't read the docs today, I don't see why they'd suddenly read a formal spec tomorrow if one is available. The problem is that Python got successful specifically in form of CPython, making the latter a de facto standard whether it wants it or not. I understand the users, too: why should they bother writing implementation-agnostic code if they're planning to run it on CPython anyway, and the vast majority of developers who might want to reuse it will likely do the same?
_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 taskI don't understand why theads get a background mode but tasks can't be fire-and-forget.
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.
Humm, no? Unless you mean ("",)
>>> not ""
Trueex:
answers = {}
answers["I exist"] = ""
if answers["I exist"]:
print("a")
does not print. answers = {}
answers["I exist"] = ""
if "I exist" in answers:
print("a")"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.
https://docs.python.org/3/library/stdtypes.html#truth-value-...
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.
> 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):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)
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.
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.
You get that behavior by using dict.get(): answers.get("I exist") returns None, which is falsy.
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.
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.
...
if answers.get('I exist'):
print('a')
Which is why you should always explicitly check for None if that is your intent. if "I exist" in answers:
... >>> 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.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?
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.
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.
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.
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.
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.
It’s crazy to suggest something this surprising wouldn’t be a problem just because it’s technically in the docs
Language and library designs should optimize for least-surprise.
[1] https://github.com/python/cpython/commit/6281affee6423296893...
[1] https://github.com/python/cpython/commit/c750adbe6990ee8239b...
Which is the behavior the parent comment asks for.
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.
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).
[0] https://docs.python.org/3/library/asyncio-eventloop.html?hig...
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.)
[1]: https://docs.python.org/3/library/asyncio-dev.html#debug-mod...
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.
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.
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.
thanks
> 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.
https://docs.python.org/3/library/asyncio-task.html#asyncio....
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?
However, I've spent some more time looking through asyncio.base_events and
* BaseEventLoop._scheduled is a list()
* BaseEventLoop._ready is a deque()
There is no change between 3.6 and 3.11 in this regard. So this could be a nothingburger if you don't use asyncgens. OTOH I suppose better safe than sorry; the only question is whether no code addressing it is more mentally taxing for the bystander than having code and trusting its implementation.
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.
The library documentation clearly calls this out, and incorrect implementations, while buggy, do not mean that async Python is itself buggy.
Latest reply from GvR is invoking Chesterton's Fence. Here's to hoping the devs can quickly figure that one out and get this fixed. Per the linked issues [1] even the stdlib asyncio implementation was affected.
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.
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.
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...
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.
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.
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.
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.
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.
https://betterprogramming.pub/common-goroutine-leaks-that-yo...
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!
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.
[1] https://github.com/python-trio/trio
[2] https://docs.python.org/3/library/asyncio-task.html#task-gro...
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.
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.
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.
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..."
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.”