As soon as anyone gets it right then perhaps async/await will be the wrong choice but not until then.
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.
>The point remains that it is possible to do these things without async/await
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.
I don’t remember claiming the UI thread couldn’t be blocked by callbacks, but it doesn’t seem onerous to avoid, and it is less onerous than having to recolor all of the code in your application and every library you use. You don’t need an async and non-async version of every library in Go, nor do you need to use hacks like “.Result” that every linter likely screams about.
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.
>I don’t remember claiming the UI thread couldn’t be blocked by callbacks
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.
I claimed that thread identity was an unnecessarily low level concern for most developers to need to worry about, not threading itself. Especially if "most developers" are just running async callbacks in a single threaded event loop, then who cares about thread identity? Not most people, that's for sure. JavaScript certainly never makes you think about thread identity, and it is one of the most popular languages for UI development. You could run a Goroutine-style lightweight threading system inside a single thread too... it's not strictly necessary for it to be as advanced and multi-threaded as Go's default implementation.
> 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.
It's not a misunderstanding on my part, as far as I can tell... if you have any "await" statements in your asynchronous callback, you automatically lose any guarantee of ordering as the runtime yields to other tasks. You do not know whether Task A or Task B will complete first. All you have control over is "critical sections", which will not be interrupted, but also can't contain any I/O (which would yield the executor) or long-running computations (which would lock up the UI) whatsoever. Even the order in which these critical sections are executed across multiple tasks is not guaranteed, so you can't rely on Task A to complete critical section 3 before Task B reaches critical section 2. It isn't ordered!
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.
The difference between async/await and pre-emption is that execution will only yield at an await instead of possibly any time. You are guaranteed that two tasks without awaits will not be run concurrently. The magic of async/await is that you can opt out of the yields by avoiding awaits...or yield when you want. You do not need lock style critical sections because every command between yields are essentially a critical section. You can't avoid pre-emption.
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.
> The difference between async/await and pre-emption is that execution will only yield at an await instead of possibly any time.
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.
Parallel and concurrent are not the same. I'm talking about preventing concurrency of UI tasks. You can guarantee A1-A2-B1-B2 instead of A1-AYield-B1-B2-A2 if you have control of what will yield. With pre-emption, A might unexpectedly yield mid-execution and B could run, even when scheduled to a single thread.
>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.
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?