What's the benefit of asyncio using weakrefs to keep track of tasks? [closed] - python-asyncio

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 7 days ago.
The community is reviewing whether to reopen this question as of 6 days ago.
Improve this question
Python docs for asyncio.create_task state:
Important: Save a reference to the result of this function, to avoid a task disappearing mid-execution. The event loop only keeps weak references to tasks. A task that isn’t referenced elsewhere may get garbage collected at any time, even before it’s done. For reliable “fire-and-forget” background tasks, gather them in a collection:
What's the benefit for asyncio to use a weakref instead of a strong reference to the created task until it is completed? As the warning above suggests, there's definitely at least one benefit to keep strong refs -- which means there's likely an offsetting benefit for weakrefs. Note that with strong refs, asyncio could, upon completion, either remove the reference entirely or switch to weakref, depending on what is required by the asyncio logic.
The use case implicit in the documentation warning is: we don't want to wait for the task (in particular, the task's return value is not used), but we do want to give the task a chance to run eventually.
Note that asyncio.TaskGroup context does keep strong references to the tasks it creates. However, it's not suitable for the above use case, since upon exit, the context blocks (it waits for all the tasks' completion). (Also, it keeps a strong reference to the task even after it completes, so it will cause memory leaks if the context lives much longer than the tasks.)

Sorry if not really an answer to the "why" - I think just whoever actually implemented it can come up with the real motivation - if there is any besides what you quote in the question.
On a second though, one can't even check if a weakrefered object has any hard-references until it is about to be switched into and executed: at task-creation, the only hard reference to it is returned to the caller.
So, once the decision was taken not to hold hard-references to tasks due to fear of resource-leakage, the "bomb" was out. And it was likely only perceived later, but them making it hold a hard reference would probably change too much of the working behavior for certain workloads.
update As a drawback to holding a hard-reference: if the tasks are created like "fire and forget", eventually done tasks would carry their result (or exception) data, and could not simply be "dropped": they'd need to be kept indefinitely, and that could be a major resource leak for a server-type app. On the other hand, were to "draw the line" that a complete task should be considered "orphan" and could be discarded? So, it appears the "line" chosen by the implementation, even if not that well though, is the task having a hard reference elsewhere.
In the source code, when a task is created, it is immediately registered as ready in an internal running loop structure - that is a hard reference - when the loop cycles through on iteration and fails to call any task there, this hard reference is dropped. That this "dropping" is not deterministic maybe is a bug that could be fixed.
/update
Maybe now the problem is getting some awareness, beyond this notice, (I've seem a twitter thread about it last week as well, with high-profile Python folks worried about the behavior), they take the step for an incompatible change there - or come up with a create_task sucessor that does the right thing. TaskGroups, as you put it, are not quite it - they are more substitutes for asyncio.gather - and even there with some drawbacks (the whole group is cancelled at __exit__ if a single task throws an exception, with no practical workaround)
For anecdote, I experimented around, and the results are really nasty with random task-drops starting at around 2500 concurrent tasks with this code:
import asyncio, random
async def blah(n):
await asyncio.sleep(random.random())
results.add(n)
async def main(m):
for i in range(m):
asyncio.create_task(blah(i))
await asyncio.sleep(1.01)
def doit(m):
global results
results = set()
asyncio.run(main(m))
try:
assert list(range(m)) == list(results)
except AssertionError:
print(f"For {m} tasks, missing {m - len(results)} results: {set(range(m)) - results}")
Including an await asyncio.sleep(0) in the loop where the tasks are created makes it all run flawlessly up to millions of tasks.

Related

time.Sleep not waking up [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 2 years ago.
Improve this question
I ran several processes on my work desktop for several days. This morning all these processes pretty much stopped working. After some debugging I found out that after executing time.Sleep, the execution flow would just get stuck there and never wake up. So while everyone on my team was freaking out I just restarted my Windows 10 PC and people thought it was a desperation reboot. I guess luckily the issue went away after restart shrugs.
I wonder if anyone has experienced this before or has any idea what may be the cause? I read in another post that time.Sleep basically schedules when execution resumes by computing the absolute time in the OS, but AFAIK the date/time settings never changed.
I realize this may be difficult to diagnose but I've never encountered this problem on non-Windows machines. Needless to say I hate Windows and am biased towards Unix, but I promise to give Windows a chance if someone can give me some reasonable explanations on this bug.
(This is not going to be an answer — for the reasons below — but rather a couple of hints.)
The question lacks crucial context.
Was the desktop put to sleep (or hibernated) and woken up — so you expected the processes to continue from where they left off?
Are you sure the relevant goroutines were stuck in time.Sleep and not something else?
The last question is of the most interest but it's unanswerable as is.
To make it so, you'd need to arm your long-running processes with some means of debugging.
The simplest approach which works in a crude way but without much fuss is to kill your process in an interesting way: send it the SIGQUIT signal and the Go runtime will crash the process — dumping the stacktraces of the active goroutines to the process' stderr.
(Of course, this implies you did not trap this signal in your process' code.)
Windows does not have signals, but Ctrl-Break should work like Ctrl-\ in a Unix terminal where it typically sends SIGQUIT to the foreground process.
This approach could be augmented by tweaking the GOTRACEBACK environment variable — to cite the docs:
The GOTRACEBACK variable controls the amount of output generated when
a Go program fails due to an unrecovered panic or an unexpected
runtime condition. By default, a failure prints a stack trace for the
current goroutine, eliding functions internal to the run-time system,
and then exits with exit code 2. The failure prints stack traces for
all goroutines if there is no current goroutine or the failure is
internal to the run-time. GOTRACEBACK=none omits the goroutine stack
traces entirely. GOTRACEBACK=single (the default) behaves as described
above. GOTRACEBACK=all adds stack traces for all user-created
goroutines. GOTRACEBACK=system is like “all” but adds stack frames for
run-time functions and shows goroutines created internally by the
run-time. GOTRACEBACK=crash is like “system” but crashes in an
operating system-specific manner instead of exiting. For example, on
Unix systems, the crash raises SIGABRT to trigger a core dump. For
historical reasons, the GOTRACEBACK settings 0, 1, and 2 are synonyms
for none, all, and system, respectively. The runtime/debug package's
SetTraceback function allows increasing the amount of output at run
time, but it cannot reduce the amount below that specified by the
environment variable. See
https://golang.org/pkg/runtime/debug/#SetTraceback.
So, if you'd be running your process with GOTRACEBACK=crash, you could be able to not only collect the stacktraces but also a dump file (on typical Linux-based systems these days this requires running under ulimit -c unlimited as well).
Unfortunately, on Windows it's almost there but not yet; still something to keep an eye on.
A more hard-core approach is to make your process dump the stacks of goroutines when you ask for that using custom-implemented way — https://golang.org/pkg/runtime/ and https://golang.org/pkg/runtime/debug contain all the stuff required to do that.
You might look at how https://golang.org/pkg/net/http/pprof/ is implemented and/or just use it right away.

where is fyne's thread safety defined?

I was attracted to Fyne (and hence Go) by a promise of thread safety. But now that I'm getting better at reading Go I'm seeing things that make be believe that the API as a whole is not thread safe and perhaps was never intended to be. So I'm trying to determine what "thread safe" means in Fyne.
I'm looking specifically at
func (l *Label) SetText(text string) {
l.Text = text
l.textProvider.SetText(text) // calls refresh
}
and noting that l.Text is also a string. Assignments in Go are not thread safe, so it seems obvious to me that if two threads fight over the text of a label and both call label.SetText at the same time, I can expect memory corruption.
"But you wouldn't do that", one might say. No, but I am worried about the case of someone editing the content of an Entry while an app thread decides it needs to replace all the Entry's text - this is entirely possible in my app because it supports simultaneous editing by multiple users over a network, so updates to all sorts of widgets come in asynchronously. (Note I don't care what happens if two people edit the same Entry at the same time; someone's changes will be lost and I don't care who's. But it must not result in memory corruption.) Note that one approach I could take would be to have the background thread create an entirely new Entry widget, which would then replace the one in the current Box. But is that thread safe?
It's not that I don't know how to serialize things with channels. But I was hoping that Fyne would eliminate the need for it (a blog post claims it does); and even using channels I can't convince myself that a user meddling with a widget in various ways while some background thread is altering it, hiding it, etc, isn't going to result in crashes. Maybe all that is serialized under the covers and is perfectly safe, but I don't want to find out the hard way that it isn't, because I'll have no way to fix it.
Fyne is clearly pretty new and seems to have tons of promise, but documentation seems light on details. Is more information available somewhere? Have people tried this successfully?
You have found some race conditions here. There are plans to improve, but the 1.2 release was required to get a new "BaseWidget" first - and that was only released a few weeks ago.
Setting fields directly is primarily for setup purposes and so not expected to be used in the way you illustrate. That said, we do want to support it. The base widget will soon introduce something akin to SetFieldsAndRefresh(func()) which will ensure the safety of the code passed and refresh the widget afterward.
There is indeed a race currently within Refresh(). The use of channels internally were designed to remove this - but there are some corners such as multiple goroutines calling it. This is the area that our new BaseWidget code can help with - as they can internally lock automatically. Using this approach will be thread safe with no changes to the developer in a future release.
The API so far has made it possible for developers to not worry about threading and work from any goroutines - we do need to work internally to make it safer - you are quite right. https://github.com/fyne-io/fyne/issues/506

How to use DoEvents() without being "evil"?

A simple search for DoEvents brings up lots of results that lead, basically, to:
DoEvents is evil. Don't use it. Use threading instead.
The reasons generally cited are:
Re-entrancy issues
Poor performance
Usability issues (e.g. drag/drop over a disabled window)
But some notable Win32 functions such as TrackPopupMenu and DoDragDrop perform their own message processing to keep the UI responsive, just like DoEvents does.
And yet, none of these seem to come across these issues (performance, re-entrancy, etc.).
How do they do it? How do they avoid the problems cited with DoEvents? (Or do they?)
DoEvents() is dangerous. But I bet you do lots of dangerous things every day. Just yesterday I set off a few explosive devices (future readers: note the original post date relative to a certain American holiday). With care, we can sometimes account for the dangers. Of course, that means knowing and understanding what the dangers are:
Re-entry issues. There are actually two dangers here:
Part of the problem here has to do with the call stack. If you call .DoEvents() in a loop that itself handles messages that use DoEvents(), and so on, you're getting a pretty deep call stack. It's easy to over-use DoEvents() and accidentally fill up your call stack, resulting in a StackOverflow exception. If you're only using .DoEvents() in one or two places, you're probably okay. If it's the first tool you reach for whenever you have a long-running process, you can easily find yourself in trouble here. Even one use in the wrong place can make it possible for a user to force a stackoverflow exception (sometimes just by holding down the enter key), and that can be a security issue.
It is sometimes possible to find your same method on the call stack twice. If you didn't build the method with this in mind (hint: you probably didn't) then bad things can happen. If everything passed in to the method is a value type, and there is no dependance on things outside of the method, you might be fine. But otherwise, you need to think carefully about what happens if your entire method were to run again before control is returned to you at the point where .DoEvents() is called. What parameters or resources outside of your method might be modified that you did not expect? Does your method change any objects, where both instances on the stack might be acting on the same object?
Performance Issues. DoEvents() can give the illusion of multi-threading, but it's not real mutlithreading. This has at least three real dangers:
When you call DoEvents(), you are giving control on your existing thread back to the message pump. The message pump might in turn give control to something else, and that something else might take a while. The result is that your original operation could take much longer to finish than if it were in a thread by itself that never yields control, definitely longer than it needs.
Duplication of work. Since it's possible to find yourself running the same method twice, and we already know this method is expensive/long-running (or you wouldn't need DoEvents() in the first place), even if you accounted for all the external dependencies mentioned above so there are no adverse side effects, you may still end up duplicating a lot of work.
The other issue is the extreme version of the first: a potential to deadlock. If something else in your program depends on your process finishing, and will block until it does, and that thing is called by the message pump from DoEvents(), your app will get stuck and become unresponsive. This may sound far-fetched, but in practice it's surprisingly easy to do accidentally, and the crashes are very hard to find and debug later. This is at the root of some of the hung app situations you may have experienced on your own computer.
Usability Issues. These are side-effects that result from not properly accounting for the other dangers. There's nothing new here, as long as you looked in other places appropriately.
If you can be sure you accounted for all these things, then go ahead. But really, if DoEvents() is the first place you look to solve UI responsiveness/updating issues, you're probably not accounting for all of those issues correctly. If it's not the first place you look, there are enough other options that I would question how you made it to considering DoEvents() at all. Today, DoEvents() exists mainly for compatibility with older code that came into being before other credible options where available, and as a crutch for newer programmers who haven't yet gained enough experience for exposure to the other options.
The reality is that most of the time, at least in the .Net world, a BackgroundWorker component is nearly as easy, at least once you've done it once or twice, and it will do the job in a safe way. More recently, the async/await pattern or the use of a Task can be much more effective and safe, without needing to delve into full-blown multi-threaded code on your own.
Back in 16-bit Windows days, when every task shared a single thread, the only way to keep a program responsive within a tight loop was DoEvents. It is this non-modal usage that is discouraged in favor of threads. Here's a typical example:
' Process image
For y = 1 To height
For x = 1 to width
ProcessPixel x, y
End For
DoEvents ' <-- DON'T DO THIS -- just put the whole loop in another thread
End For
For modal things (like tracking a popup), it is likely to still be OK.
I may be wrong, but it seems to me that DoDragDrop and TrackPopupMenu are rather special cases, in that they take over the UI, so don't have the reentrancy problem (which I think is the main reason people describe DoEvents as "Evil").
Personally I don't think it's helpful to dismiss a feature as "Evil" - rather explain the pitfalls so that people can decide for themselves. In the case of DoEvents there are rare cases where it's still reasonable to use it, for example while a modal progress dialog is displayed, where the user can't interact with the rest of the UI so there is no re-entrancy issue.
Of course, if by "Evil" you mean "something you shouldn't use without fully understanding the pitfalls", then I agree that DoEvents is evil.

Testing concurrency features

How would you test Ruby code that has some concurrency features? For instance, let's assume I have a synchronization mechanism that is expected to prevent deadlocks. Is there a viable way to test what it really does? Could controlled execution in fibers be the way forward?
I had the exact same problem and have implemented a simple gem for synchronizing subprocesses using breakpoints: http://github.com/remen/fork_break
I've also documented an advanced usage scenario for rails3 at http://www.hairoftheyak.com/testing-concurrency-in-rails/
I needed to make sure a gem (redis-native_hash) I authored could handle concurrent writes to the same Redis hash, detect the race condition, and elegantly recover. I found that to test this I didn't need to use threads at all.
it "should respect changes made since last read from redis" do
concurrent_edit = Redis::NativeHash.find :test => #hash.key
concurrent_edit["foo"] = "race value"
concurrent_edit.save
#hash["yin"] = "yang"
#hash["foo"] = "bad value"
#hash.save
hash = Redis::NativeHash.find :test => #hash.key
hash["foo"].should == "race value"
hash["yin"].should == "yang"
end
In this test case I just instantiated another object which represents the concurrent edit of the Redis hash, had it make a change, then make sure saving the already-existing object pointing to the same hash respected those changes.
Not all problems involving concurrency can be tested without actually USING concurrency, but in this case it was possible. You may want to try looking for something similar to test your concurrency solutions. If its possible its definitely the easier route to go.
It's definitely a difficult problem. I started writing my test using threads, and realized that they way the code I was testing was implemented, I needed the Process IDs (PID) to actually be different. Threads run using the same PID as the process that kicked off the Thread. Lesson learned.
It was at that point I started exploring forks, and came across this Stack Overflow thread, and played with fork_break. Pretty cool, and easy to set up. Though I didn't need the breakpoints for what I was doing, I just wanted processes to run through concurrently, using breakpoints could be very useful in the future. The problem I ran into was that I kept getting an EOFError and I didn't know why. So I started implementing forking myself, instead of going through fork_break, and found out it was that an exception was happening in the code under test. Sad that the stack trace was hidden from me by the EOFError, though I understand that the child process ended abruptly and that's kinda how it goes.
The next problem I came across was with the DatabaseCleaner. No matter which strategy it used (truncation, or transaction), the child process's data was truncated/rolled back when the child process finished, so the data that was inserted by child processes was gone and the parent process couldn't select and verify that it was correct.
After banging my head on that and trying many other unsuccessful things, I came across this post http://makandracards.com/makandra/556-test-concurrent-ruby-code which was almost exactly what I was already doing, with one little addition. Calling "Process.exit!" at the end of the fork. My best guess (based on my fairly limited understanding of forking) is that this causes the process to end abruptly enough that it completely bypasses any type of database cleanup when the child process ends. So my parent process, the actual test, can continue and verify the data it needs to verify. Then during the normal after hooks of the test (in this case cucumber, but could easily be rspec too), the database cleaner kicks in and cleans up data as it normally would for a test.
So, just thought I'd share some of my own lessons learned in this discusson of how to test concurrent features.

Core Data and threading

What are some of the obscure pitfalls of using Core Data and threads? I've read much of the documentation, and so far I've come across the following either in the docs or through painful experience:
Use a new NSManagedObjectContext for each thread, but a single NSPersistentStoreCoordinator is enough for the whole app.
Before sending an NSManagedObject's objectID back to the main thread (or any other thread), be sure the context has been saved (or at a minimum, it wasn't a newly-inserted-but-not-yet-saved object) - otherwise the objectID will actually be a temporary ID and not a persistent one.
Use mergeChangesFromContextDidSaveNotification: to detect when a save happens in another thread and use that to merge those changes with the current thread's context.
Bonus question/observation: I was led to believe by the wording of some of the docs that mergeChangesFromContextDidSaveNotification: is something only needed by the main thread to merge changes into the "main" context from worker threads - but I don't think that's the case.
I set up my importer to create batches of data which are imported using a subclass of an NSOperation that owns it's own context. The operations are loaded into an NSOperationQueue that's set to allow the default number of concurrent operations, so it's possible for several import batches to be running at the same time. I would occasionally get very strange validation errors and exceptions (like trying to add nil to a relationship) and other failures that I had never seen when I did all the same stuff on the main thread. It occurred to me (and perhaps this should have been obvious) that maybe the context merging needed to be done for all contexts in every thread - not just the "main" one! I don't know why I didn't think of that before, but I think this helped. (It hasn't been tested well enough yet for me to feel sure, though.) In any case, is it true that you need to observe that notification for ALL import threads that may be working with the same datasets and adding/updating the same entities? If so, this is yet another pitfall bullet point, IMO, although I have yet to be certain that it'll work.
Given how many of these I've run into with Core Data in general (and not all of them just about multi-threading), I have to wonder how many more are lurking. Since multi-threading so often ends up with bugs that are difficult if not impossible to reproduce due to the timing issues, I figured I'd ask if anyone had other important things that I may be missing that I need to concern myself with.
There is an entire rather large bit of documentation devoted to the subject of Core Data and Threading.
It isn't clear from your set of issues what isn't covered by that documentation.

Resources