Yep! There was a submariner-turned-astronaut that mentioned the smell was pretty similar, and also that submarine deployments can be described as "breathing recycled farts for six months." At least submarines have replacement oxygen handy!
Personally I use the emacs android port with org-roam, with syncthing for sync to back home. I've got ctrl and meta mapped to volume up and down. I don't know that I'd call it a good way, but it works well enough for me :).
IMO this is a strong argument for proper threads over async: you can try and guess what will and won't block as an async framework dev, but you'll never fully match reality and you end up wasting resources when an executor blocks when you weren't expecting.
I don’t find this argument super strong, fwiw. It could just mean ‘be wary of doing blocking operations with async, and note map makes reading memory blocking (paging in) and writing memory blocking (CoW pages)’
I think there are reasons to be wary but to me, debugging comes first (this goes two ways though: if you have a single ‘actual’ thread then many races can’t happen) because debuggers/traces/… work better on non-async code. Performance comes second but it’s complicated. The big cost with threads is heavy context switches and per-thread memory. The big cost with async is losing cpu locality (because many syscalls on Linux won’t lead to your thread yielding, and the core your thread is on will likely have more of the relevant information and lots of cache to take advantage of when the syscall returns[1]) and spending more on coordination. Without io_uring, you end up sending out your syscall work (nonblocking fd ops excepted) to some thread pool to eventually pick up (likely via some futex) load into cache, send to the os on some random core, and then send back to you in a way that you will notice such that the next step can be (internally) scheduled. It can be hard to keep a handle on the latency added by all that indirection. The third reason I have to be wary of async is that it can be harder to track resource usage when you have a big bag of async stuff going on at once. With threads there is some sense in which you can limit per-thread cost and then limit the number of threads. I find this third reason quite weak.
All that said, it seems pretty clear that async provides a lot of value, especially for ‘single-threaded’ (I use this phrase in a loose sense) contexts like JavaScript or Python where you can reduce some multithreading pain. And I remain excited for io_uring based async to pick up steam.
[1] there’s this thing people say about the context switching in and out of kernel space for a syscall being very expensive. See for example the first graph here: https://www.usenix.org/legacy/events/osdi10/tech/full_papers... . But I think it isn’t really very true these days (maybe spectre & co mitigations changed that?) at least on Linux.
Async is for tasks dominated by waiting, e.g. http serving, not computations. This means it's extremely rare to run into mmap blocking related issues if you don't do something strange.
Furthermore async doesn't exclude multi threading:
- having multi threaded worker threads in addition to CPU threads is pretty normal
- having multiple async threads potentially with cross core work stealing is also the nrom
I.e. if you just follow basic advice the huge majority of task interacting in any potential performance problematic way will not be run in async task even if you write an async web server.
> but you'll never fully match reality and you end up wasting resources when an executor blocks when you weren't expecting
and you wast tons of resources always even without doing something unusual with non async IO _iff_ it's about waiting dominated tasks as you have way more management overhead
furthermore in more realistic cases it's quite common that some unplanned blocking is mainly casing latency issues (which in worst case could case timeouts) but due async engines still using multi threading it not leading relevant utilization issues. That is if it's just some unplanned blocking. If you do obviously wrong things like processing large files in async tasks things can be different.
An argument against async is that depending what you use it can add complexity and that a lot of use-cases don't benefit form it's benefits enough to make it a reasonable choice. Through that is also a bit language dependent. E.g. JS is already anyway coperative in your program and using async makes things simpler here (as the alternative are callbacks). Or in pythons with GIL the perf. gain of async are much higher compared to the gains in idk. C++.
This kind of issue exists only in async executor implementations that cannot detect blocked workers and inject new ones to compensate for the starvation. I'm not aware if Rust has anything like this today (both Tokio and async-std are not like that) or in development for tomorrow, but there are implementations that demonstrate resilience to this in other language(s).
Unfortunately, it does not appear to look into .NET's implementation with sufficient detail and as a result gets its details somewhat wrong.
Starting with .NET 6, there are two mechanisms that determine active ThreadPool's active thread count: hill-climbing algorithm and blocking detection.
Hill-climbing is the mechanism that both Tokio blog post and the articles it references mention. I hope the blog's contents do not indicate the depth of research performed by Tokio developers because the coverage has a few obvious issues: it references an article written in 2006 covering .NET Framework that talks about the heavier and more problematic use-cases. As you can expect, the implementation received numerous changes since then and 14 years later likely shared little with the original code. In general, as you can expect, the performance of then-available .NET Core 3.1 was incomparably better to put it mildly, which includes tiered-compilation in the JIT that reduced the impact of such startup-like cases that used to be more problematic. Thus, I don't think the observations made in Tokio post are conclusive regarding current implementation.
In fact, my interpretation of how various C# codebases evolved throughout the years is that hill-climbing worked a little too well enabling ungodly heaps of exceedingly bad code that completely disregarded expected async/await usage and abuse threadpool to oblivion, with most egregious cases handled by enterprise applications overriding minimum thread count to a hundred or two and/or increasing thread injection rate. Luckily, those days are long gone. The community is now in over-adjustment phase where people would rather unnecessarily contort the code with async than block it here an there and let threadpool work its magic.
There are also other mistakes in the article regarding task granularity, execution time and behavior there but it's out of scope of this comment.
Anyway, the second mechanism is active blocking detection. This is something that was introduced in .NET 6 with the rewrite of threadpool impl. to C#. The way it works is it exposes a new API on the threadpool that lets all kinds of internal routines to notify it that a worker is or about to get blocked. This allows it to immediately inject a new thread to avoid starvation without a wind-up period. This works very well for the most problematic scenarios of abuse (or just unavoidable sync and async interaction around the edges) and allows to further ensure the "jitter" discussed in the articles does not happen. Later on, threadpool will reclaim idle threads after a delay where it sees they do not perform useful work, with hill-climbing or otherwise.
I've been meaning to put up a small demonstration of hill-climbing in light of un-cooperative blocking for a while so your question was a good opportunity:
(I’m under the impression that this was released in 2021, whereas the linked Tokio post is from 2020. Hopefully that frames the Tokio post’s more accurately.)
The docs could do a better job of spelling it out, but csexp's really do only represent "trees of bytestrings". They're not a particularly rich format. If you're looking for "transfer this lambda over the network" semantics that's obviously a lot more complicated, but there's at least some research-project Schemes that've done it. The one I know of is Termite Scheme, which builds an Erlang-style distributed process model.
The "suffix" there is a docs mistake. if you look at the example csexp's on the page the lengths are prefixed. I think that the csexp's of this package are compatible with Rivest's format.
So, "canonical" here means "for a given tree of data this is one unambiguous binary representation for it," which is a useful property for crypto signatures. It does not mean "this is a blessed way to write sexp's", though folks can be forgiven for not realizing that since the docs here don't say much about the when's or why's of use.
An EMT friend described her take as, you need empathy in order to help people, but you need to be able to switch it off because sometimes what you're about to do is going to hurt them.
It's like any of our instinctual responses. Sometimes it helps, sometimes it's in the way.
It's not really switching it off as such, it's more that you have to "long-term empathize" with the person you're helping.
Setting a broken bone or popping a joint back in to place will hurt right now, but the consequences of not doing it will hurt the person a lot more over time. So you do inflict pain on them right in the moment, which is certainly unpleasant to do, but in the hope that the overall pain and discomfort for them will be significantly lessened.
At least that's how a friend of mine (former firefighter) put it.
Of the three compilers I've worked on in-depth, only one of them had a "normal" memory management scheme.
One of them was unburdened by any thought of freeing stuff, and relied entirely on the application exiting for cleanup. This was very convenient to work with, and never ended up posing an issue.
Another used a series of allocation arenas, where certain arenas would be cleared at certain points in the compiler pipeline. This made for both speedy alloc/freeing and avoided leaks, since you weren't at risk of "forgetting" a data structure. It was also a major headache to keep track of exactly what the longest lifetime of a long-lived datastructure might be, and to pick an arena that won't be cleared in the meantime. Unfortunately the programs compiled with this compiler were large enough that we certainly couldn't have gotten away with just leaking memory; we sometimes OOMed as-is!
The third used standard C++ memory management. This compiler was quite simple, and the vast majority of its data used stack-based lifetimes. For a more complex compiler this would've become a headache.
I think that all of these compilers chose the correct allocation strategy for what they were doing. "Good practices" aren't as universal as we might like to believe, they depend entirely on the context in which a tool is designed to operate. And yes, we can guard to some extent against that context changing, but for the most part that's why we keep getting paid.
Judging by the downvotes I am getting (with zero explanation as to why), I'd say that many misunderstood the "good practices" part and took offence, as if I said there's only one good practice.
I was taught -- including at the start of my career when I used exclusively C/C++ (about 18.5y ago) -- to take care of all resources I was using and not rely on runtimes.
I understand and appreciate different usages but to me doing a proper cleanup was the sane default for most programmers. And that's all what I was saying.
Obviously, as one digs deeper in a specialised area where more and more efficiency is demanded then they have to reach for tools that most of us wouldn't normally. That's quite normal and was always interesting for me to read about.
I didn't downovte, but asides like, "sounds awful" and "nasty" definitely read as judgemental. Your other comments on this thread make you seem much more open minded than this one did.
I think parent was talking about navigation more than surveillance, so VORs and similar. Between that and inertial navigation, there's plenty of other location sources if your GPS receiver does fail.
Though if we're feeling pessimistic, we can guess at how a failure might occur. Let's say that your GPS receiver is poorly written enough that it just starts returning wrong location data on week rollover. Let's also say that that receiver is the aircraft's main time provider. Since the inertial navigation system also needs an accurate time source, it could be hooked up to this same time source. Hopefully it wouldn't be, but we're assuming the aircraft in question here is all-around subtly defective. The INS doesnt't include handling for bogus time jumps, so its speed and location tracking is also corrupted. So, you and your unfortunate copilot could find yourselves without your two main navigation sources a few hours into a trans-pacific flight, and assumedly the GPS-coupled autopilot had pointed you well off course before you'd noticed. In this situation you'd have to get yourselves reoriented based off your compass and best guess of a location. Hopefully you have enough fuel reserves after you little diversion to find land search for an airfield.
So that would be a pretty bad situation. I wouldn't worry though, aircraft systems are generally better isolated than that.