This is an exceptionally low level detail that very few people should ever have to think about. Is there something else you can point to that's pertinent to more developers?
Having written Go for years (as well as a good bit of Rust), I love not having to worry about async/await being the concurrency model, and thread identity is never something I think about. With the upcoming Go release that brings generics, I think there are some code patterns that inherently benefit from an async/await style pattern where you spawn several tasks and await their results, so I'm looking forward to having a generic pattern to spawn goroutines that return a value into a Promise... but the infectious nature of "real" async/await is not something I would consider beneficial in most languages. Rust is a language that is all about low level details and performance, so it actually makes sense there, but that's a rare exception, in my opinion.
Developers generally shouldn't need to worry about things a good runtime can take care of, just like you don't need to worry about when you're going to free an object in C#... the runtime takes care of these details that are rarely relevant to meeting the developer's objectives. When you absolutely need that control, there are usually escape hatches. If the application you're writing frequently requires unlimited control, you should probably be using an unmanaged language like Rust to begin with.
Every popular UI package uses a UI thread pattern. Every UI dev thinks about this.
As I said, Go handles this problem just fine, apparently disproving your original argument, which was the main point. Async/await with explicit yield points isn’t needed to support this pattern.
My understanding is Go UIs are mostly using html or some other languages/frameworks for UI work. Can you link me to a popular native Golang GUI framework to look at?
The point remains that it is possible to do these things without async/await, which you seemingly asserted it was not. Everything else is irrelevant, as far as I can tell.
Go absolutely isn’t frequently used to develop native UIs, so of course “popular” is a strange thing to discuss here. Why isn’t it popular? Most likely because the kind of visual UI builder tools used in Visual Studio or Android Studio have never had an equivalent funded for use with Go, due to lack of commercial support for that use case. Beyond that, web UI frameworks are immensely popular these days, and most companies would rather use those, further removing motivation to really “make native GUI happen” in Go, but there are niche use cases out there, as evidenced by the existence of libraries. If Go had come out in the early 2000s, native GUI support would likely have been a much higher priority. This is pretty far off topic, though.
My point was simply that async/await is good and useful when you're dealing with main threads. You asserted that that was a niche use case and UI programming should not think about thread, to which I replied that it was exceedingly common.
The frameworks you link to do indeed use the main thread pattern which can be blocked by callbacks into Go. You need to be aware of what code you run on the main thread even in the Go code. The problem is naturally colored. Async/await is useful in dealing with that.
If you use blocking code in C#, even indirectly, you can absolutely block the UI thread, and you may do so accidentally! One async function calls another which calls another which inadvertently does some blocking computation for a few seconds only under certain conditions, freezing the whole UI. If you instead had each UI handler spawn a Goroutine and hand control back to the UI loop immediately, then you would never have any possibility of one of those “async” handlers blocking the UI loop, because every Goroutine is preemptible, and no other Goroutine is running on the locked OS thread.
That robustness alone instantly makes Goroutines better for this. A framework could easily enforce this pattern such that the developer never has to even think about the extra steps of spawning the handler Goroutine or syncing the results back to the UI thread, if there were any demand for native UI frameworks in Go, which there really hasn’t been. I’ve written somewhat similar frameworks in Go for non-GUI purposes.
You don’t seem likely to change your opinion, so I’ll just leave it there, but… the idea that async/await is suboptimal isn’t new. It’s just hard to implement something else, so it has taken languages a long time to do it. Erlang has obviously existed for a long time. Kotlin similarly decided against async/await, and Java is in the process of moving to lightweight tasks like Go using Project Loom. I’m very surprised C# hasn’t announced any intention to move in that direction.
You had claimed that worrying about threading "[...] is an exceptionally low level detail that very few people should ever have to think about." When main threads block, UI devs do need to worry about it. Go's Goroutines do not solve it without thought.
> If you instead had each UI handler spawn a Goroutine
If you did that, you would have a mess of a UI. Like I keep trying to get across, that is not how any of these frameworks work. They are all single threaded and order matters. Implicit execution yielding is insufficient.
Kotlin is probably closer to async/await than Goroutines. Functions are marked with suspend, coloring them like async in C#. Task.Result is replaced with .await() in Kotlin. Scopes are explicitly managed and can be bound to threads, much like the fine grained management you get in C#. If C# added launch { } syntax to fire off async methods, I would not mind it at all, although I don't think it would be significantly different. The coloring remains because it is essential to the problem space.
You keep claiming implicit threading is better for UI but you have no examples. That said, you're free to like or dislike async/await as you choose. I am simply trying to inform you of a use case that you are neglecting.
> You keep claiming implicit threading is better for UI but you have no examples.
"Implicit threading" isn't causing problems here. When you are running multiple async/await tasks at the same time, you have no guarantee of ordering there either, you just get all the downsides of additional syntactic bloat and the possibility of accidentally blocking the executor because the executor isn't preemptive -- it's cooperative. If someone clicks a button and that spawns an async callback task, and then they click a different button that spawns another async callback task, as long as you don't block the executor with bad code, those tasks will run in any order that they please as time is available on the executor and as "await" calls unblock. If you're somehow only allowing (or only considering) the case where only one UI callback task is allowed to run at a time without making the UI feel locked up, then you have exactly as much control from within a Goroutine over the ordering of what happens as you would in async/await... but since you're (presumably, since you should be) in a separate goroutine from the UI thread, you cannot block the UI thread no matter what you do, even though you can if you are running a single-threaded async/await event loop and do anything wrong. A classic C# approach would be to launch a full OS thread for each callback handler, and then only synchronize with the UI thread to update UI state, and for many use cases that is arguably better than async/await. Full OS threads are typically considered too expensive to launch many of them, which is why async/await came about, but for small numbers of concurrent tasks... async/await doesn't offer much advantage over full threads.
To make this even more explicit, I would argue that a very large percentage of UI developers these days are developing SPAs, and most actual work that a SPA does is performed on the backend, not within the frontend javascript. As UI elements are interacted with, the SPA is firing off asynchronous requests through a load balancer, which does not even guarantee that requests will remotely go to the same backing server. Each server is acting as the callback handler from completely different machines, yet those developers do just fine without thinking about the thread identity of random UI handlers. This is extremely multithreaded and unordered.
The differences between classic async/await and goroutines/lightweight tasks are also a lot fewer than you seem to realize, they aren't radically different, but those differences represent significant improvements. You can emulate classic, cooperative async/await on top of preemptive lightweight threads, but you can't go the other way.
Here's your fundamental misunderstanding. You absolutely do have control of ordering. Unless you explicitly yield execution, you know you have full control of that thread and will never be pre-empted. That is a useful tool used all over UI dev and game dev.
The only apparent benefit of needing to invoke "await" to break up your implicit critical sections is if you have a crap ton of global state (beyond the UI presentation layer) that you're manipulating without any kind of explicit synchronization at all. That's hardly an inspiring design pattern. You want to talk about messy UIs... that's the kind of mess JavaScript is classically known for, since people could always rely on the single-threaded nature to store everything as a global and manipulate it without a care in the world. Global state should be used sparingly. It's not just an antipattern for maintainability reasons, global state is also generally bad for performance since compiler optimization passes can't be as aggressive around it, and if you ever are running in a multithreaded context, manipulating global state significantly hurts CPU performance as the various cores have to keep synchronizing that global state back and forth.
Perhaps you would like to explain what "control of ordering" means in your context, because not being able to control the order that concurrent tasks complete is a very clear consequence of yielding control to the executor with an "await" statement. If you never yield, then sure, but... that's not very useful in an asynchronous context. You might as well just go back to writing blocking C event loops where every task always runs to completion before any other task can start.
And yes, you _can_ guarantee task order A then B by having Task B await A.
However, you're focusing on task order, I'm mostly talking about the order of instructs of a tasks acting like critical sections. Said another way, async/await lets you queue several critical sections in a convenient way. It also gives you the tools to do this on specific scheduling contexts and without thread marshaling or synchronization.
You can bemoan the fact that UI systems are designed around single thread access, that that's messy or whatever but its just the reality. You mention thread synchronization. Exactly so. One of the reasons these frameworks opt for a single UI thread! Every popular UI framework works this way. Dealing with UI threads is inescapable. Running code on a UI thread to access UI state in a serialized way is extremely common and imo async/await handles it well and implicit threading doesn't.
Yes, that’s a critical section.
Every thread on your computer is constantly being preempted by the OS thread scheduler. It doesn’t matter to your code, just like it doesn’t matter in a preemptive task system, unless you are manipulating global state without a lock and someone else is trying to manipulate it behind your back. Preemption is a feature, not a bug.
> And yes, you _can_ guarantee task order A then B by having Task B await A.
What…? The whole point is that these are separate tasks started by the GUI framework as asynchronous callbacks. They can’t wait on each other because they don’t know about each other. If you think Go code can’t run its own instructions in a specific order… what?! So of course Go could do that too. But that’s not at all what I’m discussing!
> However, you're focusing on task order, I'm talking about the order of instructs of a tasks acting like critical sections.
I just don’t feel like you know how Go works. The order of instructions is the order you write them, barring any funny compiler optimizations which also apply to C#. It’s not “magically” running all instructions of your function in parallel or something. Whether the task gets interrupted or not is irrelevant — it will resume where it left off. As long as you synchronize access to global state, no one will be observe the interruptions to the task, exactly like how your operating system is interrupting your program constantly and you can’t even tell.
“Implicit threading” isn’t even the right term here, since that implies the compiler or runtime is automatically forking your code into parallel sections for efficiency. Go does not do this. If you write a for loop, it executes every loop iteration sequentially. It doesn’t do wacky things like you seem to believe.
> You can bemoan the fact that UI systems are designed around single thread access, that that's messy or whatever but its just the reality.
That’s not at all what I’m “bemoaning”. If that’s what you’ve gotten from this conversation, then I’m utterly bewildered. I’m done trying to get my points across if communication has broken down to this degree.
>Whether the task gets interrupted or not is irrelevant — it will resume where it left off. As long as you synchronize access to global state [...]
Cooperative multithreading like async/await is leveraging the fact that actually if you control the interruption, THAT can be your synchronization. You don't need locks. UI programming is usually lock free. A UI thread is more commonly used. Understanding that, you can see that pre-emption _is_ sometimes a bug.
>>>> If you never yield, then sure, but... that's not very useful in an asynchronous context. You might as well just go back to writing blocking C event loops where every task always runs to completion before any other task can start.
What you're talking about isn't what most people talk about or experience in regards to C# async, and it's still not an actual benefit. In all cases described so far, you're better off explicitly writing A to call B than to try to misuse an async task executor as a weird queue, especially as you said yourself that there is no yielding involved. It's really that simple. What you have described is incredibly brittle code riddled with implicit dependencies, and since you're not yielding, you are locking up the UI.
...But anyway, forget it. I'll be more than happy to use a multi-threaded UI system when such a thing exists.
In other words it's a fully capable systems level language as well as an application language.
There are a number of design choices that people use to writing high level applications think are funny but when you reflect on them they are there to allow low level control for systems level or high performance programming.
I used to be baffled by some of the designs until I realised that it's a language for everyone/every system, not just for what I personally am doing right now.
I agree it is a very capable language, but not every aspect is perfect. I would generally prefer C# over a lot of the alternatives.
- is function coloring better than alternatives?
- is it possible to interact with APIs that require they be called from a specific UI thread using those alternatives?
The answers are resoundingly “no” and “yes”. The point is that I think C# should at some point adopt lightweight preemptible threads similar to goroutines for the benefits, not that you need to switch to Go for GUI apps on Windows. I don’t agree at all that function coloring for async is a benefit, or that poorly written tasks being able to block the executor is a benefit, which is what the GP basically argued.
C# is a good language, but defending function coloring feels the same as those Go developers who defended not having generics for years. It’s important to be able to recognize the shortcomings in languages so that improvements can be made. Generics will be a benefit to the Go language, even if it took a surprisingly long time to materialize. Async in C# is better than what it had before, but that doesn’t mean it is perfect.
To your question more directly, Go’s runtime automatically uses the underlying native async APIs in most places where it makes sense in the standard library. WinRT probably isn’t used by Go, but I haven’t ever looked. If you’re specifically referring to UI APIs, then this is irrelevant to the discussion, because I’m not saying you should switch to Go for that. Plus, Google doesn’t care about using native windows GUI APIs from Go, so it probably wouldn’t fit your definition of “natural”, since I doubt anyone has put in the effort to clear that bar.
I'm well aware of the shortcomings of coloring, but the point remains that it's the only way to do async that does not result in insular ecosystems with a penchant to rewrite everything so as to gain the benefits of their bespoke green threading (i.e. exactly like Go).
This may change if we can even standardize green threads on OS level, such that all languages can target it in an interoperable way. But the experience with the adoption (or rather lack thereof) of fibers in Win32 shows just how difficult this can be.