You need to keep the UI responsive, so you hand off all resource or cpu heavy work off the UI thread, give back control to rendering and responding to user input ... and later need to collect the results in the UI thread to change the UI state.
It is about doing things in parallel. There are many ways on how to achieve that, I am just nit picking on performance.
I feel like I could untangle some this, but not without re-writing a version of the article that this thread is about. Like async/await adding overhead and therefore being slower than direct calls, if you use them incorrectly, is actually accurate but not a legitimate problem (they make up for their overhead when used as designed, specifically on slow endpoints).
Also, async/await doesn't cut you off from anything. You just need to understand what is and isn't an asynchronousable end-point, people get confused then start throwing random keywords around without really grasping how it works (e.g. "if I await everything, it is concurrent, right?!" then write a bunch of code that actually runs synchronously but with tons of overhead).
I guess this is my nice way of saying: You need to read this very article. It corrects your concerns, and shows you when async/await is useful and likely when it isn't.
> It's important to have ConfigureAwait(true) ... Otherwise you should use (false)
This is not universal. In modern ASP.NET core (i.e. web services), ConfigureAwait does nothing and is not recommended. I can confidently say that I don't want it in any of the code that I work on. We can make blanket statements, but clearly we do different things using different patterns and one size does not fit all.
Micro-frameworks that don’t pack the kitchen sink like nodejs became popular, so Microsoft made .NET Core to be philosophically similar to that.
Because people such as yourself got confused by this, they eventually stopped pushing Framework and renamed .NET Core to .NET.
Microsoft has never been popular with the valley crowd, so everything they have ever done is pooped on constantly by people on here.
You should check out some of the posts on here when VS Code came out. A lot of people were amazed that Microsoft had built something decent “out of nowhere” like those people hadn’t just been living in a bubble their whole career.
And then... I don't know, .NET Framework to JDK, or something like that?
I like the whole thing (since .NET now runs on Linux well enough, C# feels okay as a language and has good tooling, like Visual Studio or JetBrains Rider) but there's definitely some naming and branding confusion sometimes going on, including in my own head.
> Micro-frameworks that don’t pack the kitchen sink like nodejs became popular, so Microsoft made .NET Core to be philosophically similar to that.
.NET Core was still a very batteries-included library, it just dumped bits tied to Windows and made some other backward-compatibility sacrifices to get something cross platform and open sourcable.
> Because people such as yourself got confused by this, they eventually stopped pushing Framework and renamed .NET Core to .NET.
IIRC, they announced the long term plan to move all functionality that would be retained into Core and converge on a single implementation fairly early on, but to move forward in parallel until then .NET Standard versions were created to identify compatibility across Core and Framework during this period.) Dropping Framework and making Core just unqualified . NET (and eliminating the need for Standard) was just the completion of that strategy.
Historically maybe-sorta; the consumer download for Java used to be called the JRE. It hasn't been updated since Java 8. .NET ships with a pretty full toolkit even on consumer systems (it's what undergirds PowerShell too) so .NET probably maps better to the whole-hog JDK.
The JRE-equivalent in the .NET stack is the Common Language Runtime. The JRE processes Java bytecode; the CLR processes Common Intermediate Language (CIL, formerly MSIL) instructions.
It's this type of stuff that causes me to prefer the Sync implementations over the Async wherever I can. Did I get it right? Is it going to deadlock in production? How can I be sure? Who knows? Straight threads are much easier for me to understand than async in C#.
Except you don't need anything .Net specific to be installed on the end user machine. I build and deploy single-file C# binaries to Linux all the time. There's no longer a dependency on having the .Net framework installed or having dozens of DLLs accompanying your binary.
sn_master is talking about a "self-contained, single file deployment" that does not need an existing shared runtime present on that target machine (or container). This is an orthogonal concern to running on Linux, which could depend on a shared runtime, or not.
Self-contained deployments are platform-specific.
A deployment that depends on a shared runtime can be cross-platform, since all the platform-specific parts are in that shared runtime installed for that machine's platform.
One of my favorite things about .NET now being open source (along with the frameworks like ASP.NET) is that I can easily 'peek under the hood' for various functions that I use for edge cases/details. For example, with int.TryParse(myString, out int i), what happens if myString is NULL? The docs for most of the core .NET libraries are pretty good, but nothing beats quickly parsing through the actual code. This is especially true for various constructs in ASP.NET, where the documentation frequently lags behind newer versions, and by nature, can't capture all of the possible options.
Digging deep into the core libs also makes you a better programmer by renforcing idiomatic patterns and exposing you to new techniques. Just the other day I dug into how ArgumentNullException.ThrowIfNull works, and discovered the newish (C# 10) attribute [CallerArgumentExpression("argument")] which gives you a string with the expression used to pass that parameter (i.e. in most cases, just the original variable name.) This has now entered my mental toolbox for stuff like validation code. The docs for this are actually quite good (https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...), but one has to know it's there. Often, reading other code is the best way to discover new features/techniques, and what better code to read than core .NET itself.
Hacker news is just too keen on shitting on .NET
In my experience switching from Java to .NET was an epiphany. The language is 10x more concise, async/await is the same as in javascript, the documentation and official guides are miles ahead of Java, and it's just a pleasure to work with.
C# is an amazing, well thought out language, agreed.
> async/await is the same as in javascript
How many variations of async/await coroutine programming are there? Not saying I disagree, but I think most of them are similar.
> the documentation and official guides are miles ahead of Java
This is where you lose me. It's been a few years, but the last time I looked at dotnet docs it was a steaming pile compared to most other languages I've looked at, including Java.
The moment you switch from C# to e.g C or CPP or other dev unfriendly tech is the moment you realize how c# lang design team and std lib API design teams outperformed
I spent a few minutes looking at a cmake script the other day and had some realizations about the industry.
The simplicity of:
dotnet build MySolution.sln
Is the only reason I am still doing software for a living. I am completely over my tools. I just want to solve problems and build things now. If I have to lose 5% perf because I'm lazy, so be it. I'll hire someone to obsess over the 5% later on - and only if the customer complains.
I love .Net. It’s my favorite language and you can tell that it was written by competent adults that know how to design a language and the surrounding framework. I used .Net from 2007 with Windows CE/Compact Framework all the way to 2020.
That being said, I would never tell am aspiring developer, especially one with a CS degree to learn it.
Besides, Microsoft, it’s not used heavily in most companies that pay top of the market and it’s definitely not the language that I would chose to use if I had to go through a generic coding interview.
Yes, I’m well aware that most of the 2.7 million developers in the US are “enterprise developers” and many use .Net. While .Net has been cross platform for years, it’s still the choice of mostly old school Windows shops.
There is a very sizable group of game developers who use C# thanks to Unity so it’s actually pretty common advice for entry level game devs to learn C#.
I'd tell 'em to try it for 6-12 months and then take a mandatory vacation in F500
Game dev was like a kind of inspirational boot camp for me. I got to see (for better or worse) what the true limits of a professional developer and artist were.
I couldn't even stand it for a year, but it shaped my attitude towards work and hard things ever since.
People use that blog post as a slam dunk over the years, but my reaction has always been "yes, fine, but that's a small price to pay".
For example, very rarely in my experience does a function switch between synchronous and asynchronous. So when it does happen and I have to update upstream call sites, it's not so frequent nor painful enough to damn the whole abstraction and then go "see? it sucks".
When people suggest an alternative abstraction like Go, I compare my 50 lines of WaitGroup code in Go to things like `await Promise.all([prom1, prom2])` in Javascript. It's all just trade-offs.
I think we're talking past each other. There is a semantic difference between `asyncFoo()` and `blockingFoo()`, but not between `await asyncFoo()` and `blockingFoo()`, and not between `asyncFoo()` and `spawn blockingFoo()`.
There's obviously a difference between launching an operation to run concurrently with the spawning code and launching an operation and awaiting its completion, but that difference does not require the introduction of an async/await feature, and is perfectly expressible with nothing but threads and synchronous code. In particular, there is no need for two separate syntactic domains between async and sync subroutines. Indeed, in Erlang, Go, and Java, all the same behaviour of async/await is achieved with no such need, and no need for different kinds of APIs.
> Of course there is. The former constraints the type (or return-type) of the function it is used in, the latter does not.
That's not a semantic difference, but a syntactic one. Syntactic differences are expressed by which programs are accepted or rejected by the language; semantic differences are expressed by programs exhibiting different behaviours.
To show a semantic difference you need to find P and Q such that when the value of P is the same in the two programs, the value of Q could be different; that's what expresses the semantics of the code between P and Q. Again, there are some (e.g. P: `(t = currentThread(), true)`, Q: `currentThread() == t`), but they're purely technical.
> I don't remember saying anything about async/await in my original post though.
You said that the lack of need of a syntactic kind for asynchronous routines is a fallacy. It is not. Everything that you can describe with sync and async routines your can describe, with the same ease and semantics, with just sync routines and threads. There is no need for a special syntactic category of asynchronous subroutines.
> Everything that you can describe with sync and async routines your can describe, with the same ease and semantics, with just sync routines and threads. There is no need for a special syntactic category of asynchronous subroutines.
I agree with that statement.
> semantic differences are expressed by programs exhibiting different behaviours.
It can cause different behaviour whether code runs on the same thread or on a different one (see one of my other posts here). That doesn't have to happen in every program and it doesn't have to happen on each execution, but that still makes it a different behaviour - at least in my terminology.
> You said that the lack of need of a syntactic kind for asynchronous routines is a fallacy. It is not.
That's not what I said (or at least not what I meant). I meant that it is doomed to fail to try to generally unify sync and async code on a language (or runtime) level.
When people say a language "unifies sync and async code" all they mean is that there is no distinct syntactic category for "async routines". Rather, asynchronous operations are expressed as regular (i.e. synchronous, blocking) methods spawned on another thread. Either you agree that there is no loss in what you can express or believe it is doomed to fail. I don't understand how you can do both.
Yeah it depends on what you're doing. If you're doing a lot of IO probably makes sense. Not running long tasks tho. I don't think that's a good use in Python at least.
> You should elaborate. I just want to read a file, program flow should stop until it is done. sync or awaited async call for this semantically sounds exactly same to me.
What if you are reading the file and somewhere else in the code there is some logging happening. Should the logging flow stop until reading the file is done? Should it not? If not, what should happen if the "thing" (e.g. thread) that executes the code to read the file crashes?
Go does not treat sync and async the same. It specifically has syntax to deal with async, e.g. go routines.
C# is an awesome language and I've worked professionally with it on and off over the course of my career. I'll never take another job at a shop that uses it though. Every job I've taken where it was the primary language forced windows on me and a whole bunch of "enterprisey" BS. They also tended to be spots where the engineering dept was considered a cost center and not a value creation unit. This is just my experience of course but I've got anecdata from colleagues which correlates. I even recall reading about the stigma attached to C#/.NET in "Cracking the Coding Interview" so there must be something there...
I also found that startups (my preferred work env) tend to filter you out when you have too much of that on your resume which is super shitty IMO but it's a thing. I'd love to see that change. Until then I guess I'll stick with Kotlin and TypeScript.
Linking a language to "enterprisey", especially a young language like C#, seems absurd to me. There are plenty of startups using Java, C#, etc. You don't have to be riding the Node.js bandwagon to be agile.
I agree! There are a LOT more startups that use Java than C# though despite C# being a better language in almost every respect. There’s a stigma attached to it right or wrong.
I think some of that stigma is warranted because of the tooling and ecosystem around C#. The documentation contains frequent references to azure. Microsoft continues to place features into visual studio while making VS code actively bad. Many libraries are not free open source and you have to deal with weird licensing. Other ecosystems are definitely better.
Kotlin is still rather niche outside of Android. Java has been evolving but most enterprises are very reluctant to move past Java SE 8. Lots of projects haven’t even migrated to Java 11. With Oracle’s extended support that gravity is only exacerbated. All while the upstream pushes for Java 21, with confusing licensing changes along the road Eclipse Temurin gaining traction over OpenJDK. Java is a very fractured ecosystem. Microsoft has done a great job at unifying .NET, not without crossing the line several times though.
In my observation, more and more startups have been taking off with C# and .NET. It’s very productive and efficient. It truly shines in many popular benchmarks. With .NET, many potential choices have already been made for you, it’s a true framework. You get a great setup out of the box which you can tune to your liking. It’s like Angular. Java is like React - there’s a core library and the rest is on you, Spring to the rescue, or Quarkus, or Micronaut, or… a hundred other frameworks. Very flexible but also quite fragile.
The CLR has much fewer knobs than HotSpot or OpenJ9. I like that actually. It’s just more efficient in most cases. Can’t speak to multi-terabyte heaps with CLR though, haven’t been there.
The GUI story with .NET is amazing. Other than MSFT’s libraries, you have Avalonia and Uno as popular alternatives. Avalonia is very snappy. Uno has a great potential. Both are multiplatform (handheld and desktop devices, with the same codebase). Then there’s Blazor for the web in various incarnations and MAUI on Xamarin. Kotlin’s (Jetpack) Compose Multiplattform is still a couple years behind Avalonia and perhaps Xamarin. And JavaFX GUIs have never been this good or resource efficient, there are a couple other Java frameworks but they are apparently financially too resource constrained to compete yet.
As for the enterprise, I’ve seen both, retrograde Windows shops reluctant to change, riding a slow-paced product lifecycle, as well as agile projects upgrading their .NET stack a few days after the most recent GA release, often having run CI on RC releases well ahead.
Perhaps it’s location dependent. Denmark being an outlier is virtually exclusively .NET. In China, Go is very popular, you also see many contributors to Go projects on GitHub from China. Same with Vue. In short, there are also socioeconomic idiosyncrasies to it.
> So it’s basically a managed group of tools and libraries to let you build stuff in a handful of different languages, usually C#, but targeting the same runtime, which is a JIT?
This is not wrong.
.NET Framework = the built-in class libraries. Confusingly, also ".NET Framework" can mean the older, legacy, Windows only version of the whole thing.
ASP = the web server and web services centric parts of the framework.
NuGet = package management for built-in and 3rd party libraries.
Roslyn = the C# compiler
CIL : the bytecode "Intermediate Language" emitted by that compiler
CLR = Common Language Runtime for CIL, and related concerns such as GC and JIT
So I suppose "ecosystem" is correct.
C# a general purpose high-level language, OO-first but with some functional things.
The other problem is that it's broad: a person can use C# and.NET for one thing (e.g. Web services, or Desktop GUIs), and have strong opinions about what's needed and how to do things, and have them validated daily with all colleagues, assume that this is universal but ... those opinions are not true of the other.
The message map macros were the equivalent of the switch statement in a WndProc. They couldn’t add every Windows message as a virtual override because every derived class would have needed thousands of vtable entries. At the time memory was still very limited (4MB systems for Windows 95 and they also had support for 16 bit Windows where it would have been even worse).
Also the template support in the compiler was limited at the time. Later on there were other libraries that used templates like ATL and WTL.
You did get a lot for free from MFC when implementing document editing applications for OLE support, but that was an increasingly small portion of development.
I think Borland went a different way with compiler extensions for producing a Win32 application with their OWL library.
I don't think you can ever make it not leaky, especially if native resources or certain types of locks are involved at some point. This would also apply to green threads, which means you're still using async/await - it just looks completely synchronous.
It's a massive improvement over callbacks when using an event loop model too. I've worked with code that has an insane amount of nesting because it was written before async/await and instead had to pass in a callback every time it went off to do an asynchronous operation. It made the code very hard to read and it complicated exception handling, resource lifetime management, and even simply iterating over an input.
With async/await the code ends up structured the same as if the operations were synchronous. It's much easier to follow and modify.
Trading this for simplifying the remaining 99% of my code is a tradeoff I'm willing to make.
The vast majority of projects don't use CGo, and of those that do, the majority doesn't call CGo in a tight loop, making the performance impact mostly irrelevant.
So it's really coming back to the performance-orientedness of a language.
The practical end result is an insular ecosystem that tends to rewrite code "in pure Go" rather than reusing existing libraries. To the point where this attitude resulted in the standard library of Go breaking on occasions because they tried to go around libc on macOS and use syscalls directly (which are not guaranteed to be stable).
.NET is the opposite - it was designed from the get-go from cross-language and cross-runtime interop. Back when Java folk still had to struggle with JNI to use anything native, .NET 1.0 had P/Invoke and COM Interop. And now that it has async/await, it's all mapped down to a C-compatible ABI for interop purposes, so you can easily write async code in C# that calls into async code written in C++ that calls back into C# etc.
Used Sitecore at my last job, so I'm curious what they're migrating to. All I could find mentioned .NET Core. Do you have a link that covers what they're moving to?
Most of the new acquisitions are based on Java, with exception of Order Cloud and Content Hub.
.NET Core is available for XP headless rendering SDK, however it is NextJS/JSS that are getting the spotlight and there is plenty of DIY if you will go down .NET Core, specially after the cooperation agreement with Vercel.
Classical XP is stuck on .NET Framework and XM Cloud customisation points favours WebHooks instead of writing .NET code.
CDP, Search, Send are low code tooling, only exposing Web APIs, and JavaScript.
Content Hub does allow for some use of C# in scripting, but it is mostly about Webhooks as well.
No there isn’t. Elixir, and by extension Erlang and the BEAM VM, doesn’t have the concept of stackless coroutines.
Task.async/1 (or Task.async/3) at a high level just spawns another BEAM process links it to the currently running process, sets up a process monitor, and then runs. Task.await/2 just pulls from the mailbox of the current process for a returned message.
There is a semantic difference between just running a line of code or running ith with Task.async right? So then there is a distinct concept of sync vs async.
The BEAM VM doesn't have faculties to accommodate any other form of concurrency other than synchronous, pre-empted green threads. Put simply, there is nothing other than synchronous in Elixir (edit: or Erlang or any other BEAM VM language). The Task module is basically just convenience functions (edit: convenience functions around existing BEAM VM functions for spawning BEAM processes or Erlang threads) meant to facilitate short term execution of, well, tasks.
Task.async/3 and Task.await/2 are, from the thousand foot view, the equivalent of:
parent = self()
spawn(fn -> send(parent, do_something()) end)
receive do
result -> IO.puts result
end
In Elixir/Erlang parlance, I have just spawned a thread where the BEAM VM handles both the internal data structures to facilitate concurrent-safe communications between threads and the scheduling in userspace.
Put more generally, the mentality of concurrency in Elixir/Erlang is the equivalent as that of traditional pthreads in C.
I think you are misunderstanding me, so let me ask a simple question:
Is it (except for performance) always 100% equivalent whether a calculation is done "locally" or whether it is ran on some other, potentially just spawned, thread and then retrieved from this thread?
If your answer is "no it is not always 100% equivalent" then we agree (and I think we do).
No, they're equivalent. It makes no difference whether I calculate in the current process, spawned a process, started a process via the Supervisor, Task.async, or what not. They are all equivalent in the sense that they use BEAM processes.
An Elixir, Erlang, or any other developer using the BEAM VM (or at the very least myself since I don't want to generalize) doesn't make a semantic distinction between spawn and Task.async/3 because there is none. The significant differentiation is whether or not the caller is expecting a returned value, if not then Task.start_link can be used instead.
I don't need to worry about function colouring because there is none. I also don't need to worry about async vs Task.run like in C# because in Elixir there is none. To be asynchronous in Elixir/Erlang/etc... is to be synchronous. To equate it to modern backends, we basically have a microservice but every service can directly call each other.
I think I also might have been talking around you a bit, the BEAM VM sets itself apart from C#'s async/await or the TPL due to its implementation of the actor model. This video by Sasa Juric can explain better than I can the general overview of the BEAM: [1].
But a rough tl;dw, a BEAM process is to the BEAM, what an OS process is to the OS. We don't define a main function as the process supervisor in BEAM spawns processes we define instead and those are our "entrypoints."
> They are all equivalent in the sense that they use BEAM processes.
Sure, but that was not my question. 100% equivalent means that there cannot be any observable change in the behaviour of the whole program/application (except for performance).
Or in other words: is the sole purpose of the existance of `Thread.async` (or spawn, pick whichever you want) to change performance characteristics? If not, what is the purpose of its existence then?
> Sure, but that was not my question. 100% equivalent means that there cannot be any observable change in the behaviour of the whole program/application (except for performance).
In a vacuum (i.e. via spawn/1 or spawn/3) there is no observable change in behaviour throughout the whole system.
If they're linked via spawn_link then a child process crashing means the parent process dies with it unless handled in some way.
So ultimately, no there isn't observable changes. My logging process doesn't care or even know that my WebSocket process crashed.
> Or in other words: is the sole purpose of the existance of `Thread.async` (or spawn, pick whichever you want) to change performance characteristics? If not, what is the purpose of its existence then?
As stated before Task.async/3 is just a convenience wrapper around low-level BEAM primitives, there isn't anything special about Task.async/3 that you couldn't do via spawn.
The reason being that the BEAM VM schedulers prioritizes overall latency of the system over throughput by enforcing reduction calls (parlance for a function call, but specifically for tracking looping in the BEAM) at around 4000 calls, then it moves the current process back into the queue; repeat ad infinitum.
So you don't get massive speedups just spawning a new BEAM process but your independent units of execution still enjoy the same overall latency as before. If you want performance increases in the BEAM you would need to jump to dirty schedulers and NIFs which carry their own dangers.
> In a vacuum (i.e. via spawn/1 or spawn/3) there is no observable change in behaviour throughout the whole system.
Okay, let me follow up on this one. So could we take arbitrary Erlang/Elixir programs and automatically/mechanically rewrite them to push previously inlined calculations to be run on a different process (using spawn) or the otherway around - and that without causing any observable difference in behaviour in any situation except for difference in performance?
> As stated before Task.async/3 is just a convenience wrapper around low-level BEAM primitives, there isn't anything special about Task.async/3 that you couldn't do via spawn.
You can likewise answer my question for the BEAM primitives (like processes/threads): is the sole purpose of the existance of those primitives to change performance characteristics? If not, what is the purpose of its existence then?
Also, just so that we understand each other: I'm a big fan of the BEAM. This is not a criticism or anything; I just want to explain why I believe that function coloring (or however you call it) can never be conceptionally be removed from a language if sync/async is to be supported.
> Also, just so that we understand each other: I'm a big fan of the BEAM.
We probably should've established this way earlier, sorry if I came across as pretentious or off putting.
I'll answer in a bit of reverse order.
> I just want to explain why I believe that function coloring (or however you call it) can never be conceptionally be removed from a language if sync/async is to be supported.
To respond in more of a roundabout way, why doesn't C have colouring issues if it too supports asynchronicity (I'm pretty sure I've made up a word but hope the point gets accross fine) via pthreads?
The colouring issue is, at least under my understanding of C#'s async/await system is to accommodate for the fact that Rosyln transforms the C# source into .NET IL with a state machine. It's, to me at least, just a consequence from what layer of the stack we talk at. Erlang doesn't need to worry about since the runtime was designed to run with this specific model of concurrency in mind; but if we look at C#, .NET only supports using OS threads (though David Fowler is investigating green threads in the, iirc, labs repository on the DotNet Github) the async/await is, for lack of a more better word, bolted on. It's to my understanding that IronPython (or maybe it was PythonNet) has difficulties calling into C# async for that reason; though if I'm wrong please correct me.
> is the sole purpose of the existance of those primitives to change performance characteristics? If not, what is the purpose of its existence then?
Fault tolerance, latency, scalability, perhaps minor throughput increases (yeah this is a bit of a contradiction to what I said earlier, but I've revised my opinion that you wouldn't see massive performance gains unless resorting to NIFs).
The Phoenix Framework's 2 million websocket connection challenge I think is a best demonstrator for the use case of BEAM processes. A specific websocket connection dies? No problem, the Supervisor respawns it. Two million connections receiving a Wikipedia length article on a topic without the entire system coming to a crawl? No problem. There isn't a need to worry about kqueue, poll/epoll, or IOCP here, just fire off a task for the purpose and let it do it's thing.
But overall you wouldn't spawn a BEAM process just so you could compute the matrices behind RAID6/RAIDZ2, you'd delegate those to NIFs. The biggest gains in overall performance came from BeamAsm instead.
> Okay, let me follow up on this one. So could we take arbitrary Erlang/Elixir programs and automatically/mechanically rewrite them to push previously inlined calculations to be run on a different process (using spawn) or the otherway around - and that without causing any observable difference in behaviour in any situation except for difference in performance?
Yes. Using the case of the 2 million websocket connection challenge earlier, it doesn't really matter if each of those connections spawned another process as part of its routines, the other processes don't know and don't care. Taking a more generic case of Elixir's GenServers (or gen_server in Erlang), when I perform a call (as in the GenServer behaviour) it doesn't matter to the calling process what happens behind the scenes, it just blocks and waits for a response. The GenServer could fire off any number of processes it wants but the calling process doesn't care about that.
> why doesn't C have colouring issues if it too supports asynchronicity
But it does have those issues. In fact it is worse than in most other languages - just because there is no distinct support of async in a language does not mean it doesn't have async capabilities. C does have those, you just don't have any language support whatsoever, you have to take care yourself. Delegating the execution to the OS doesn't change that.
For example, if you want to transform elements in an array concurrently, you now have to use pthreads. You cannot do that while using the same code as before (which is would removing function coloring is all about).
Maybe we have a misunderstanding here: I'm talking abouyt sync/async not the async/await syntax that many languages have. The latter is to help deal with colored functions and the former is what enables the existance of concurrency. Any language that is - in one way or the other - capable of running code concurrently is suffering from that problem. It can be avoided by enforcing synchronous execution only, which some languages do (especially some DSLs), but most general purpose languages support concurrency.
And once they do, there's two choices: the programmer has to deal with it (using Thread.async, spawn, pthreads, go channels, promises, ...) or they don't. If they don't then sync and async code must look exactly the same (or be able to be converted forth and back automatically). And this goal (either making it look the same or converting it automatically) is just not solvable in general. It has been tried for decades.
For instance, check the paper "A Critique of the Remote Procedure Call Paradigm" from 1988. I think it was from Tanenbaum. It lists some of the problems that just cannot be solved in general. Some programs will always behave different after the automatic conversion, it's impossible to prevent that.
> > Okay, let me follow up on this one. So could we take arbitrary Erlang/Elixir programs and automatically/mechanically rewrite them to push previously inlined calculations to be run on a different process (using spawn) or the otherway around - and that without causing any observable difference in behaviour in any situation except for difference in performance?
> Yes.
What I'm trying to explain is that you are mistaken. It just is not possible. Maybe in specific cases, sure, but not in a safe way for every usecase that can be thought of.
And neither Erlang nor Elixir (nor the latest BEAM language, I forgot the name), luckily, try to do that. They give this controller to the developer and let them deal with the decision of how and when to use concurrency. They just try to make that as easy as possible, but they don't try to make sync and async code look and be the same.
They are not equivalent. Running a function in another process incurs the overhead of copying the data to the other process (potentially over the network). In both directions. Task.async introduces extra necessities like a linkage between the two processes, a timeout receiver, etc, and the result receiver.
Moreover, there is a context switch and it's more likely the code will run on a different core if you run it async than if you run it in the same process (which is very likely but not guaranteed) to run on the same core.
The way in which they are equivalent, is that the code that you write is identical, and the bytecode that gets run, and the existence of implicit yields is identical between the async and "sync" code.
> They are not equivalent. Running a function in another process incurs the overhead of copying the data to the other process (potentially over the network). In both directions.
> Moreover, there is a context switch and it's more likely the code will run on a different core if you run it async than if you run it in the same process (which is very likely but not guaranteed) to run on the same core.
Yes I agree, but the question as presented by valenterry asks us whether or not there is some existing semantic difference and not under the confines of performance (both gains or losses). Regardless what you have stated are all true.
But as an aside, and not directed towards you, Task.async/3 doesn't do anything that the developer cannot already do. Even in the tutorial for the learning Elixir, the fledgling developer is exposed to the different mechanisms that power Task.async/3, the source code [1] reflects this, although supervisors are covered much later down the line. The documentation for
> The way in which they are equivalent, is that the code that you write is identical, and the bytecode that gets run, and the existence of implicit yields is identical between the async and "sync" code.
And just to add on for those not familiar with the BIFs, receive, which is what Task.await/2 and Task.yield/2 use under the hood, yields execution. NIFs are another one.
Everything in the beam is "low-level" async with implicit yield points at function boundaries. Task.async just gives you a convenient future so you can get the value back, instead of the beam throwing it away (goroutines also throw away the result). You could also write that boilerplate by hand. There are also other nice things that Task.async gives you but that's not relevant to this discussion.
IOW, async/await in the BEAM is a high level thing (like goroutines) which is one of two reasonable choices, the other reasonable choice being "what zig did".
74 comments
[ 4.9 ms ] story [ 121 ms ] threadIt is about doing things in parallel. There are many ways on how to achieve that, I am just nit picking on performance.
Go is asynchronous-first, it just doesn't use the (flawed) async-await semantic to model asynchronicity.
Also, async/await doesn't cut you off from anything. You just need to understand what is and isn't an asynchronousable end-point, people get confused then start throwing random keywords around without really grasping how it works (e.g. "if I await everything, it is concurrent, right?!" then write a bunch of code that actually runs synchronously but with tons of overhead).
I guess this is my nice way of saying: You need to read this very article. It corrects your concerns, and shows you when async/await is useful and likely when it isn't.
This is not universal. In modern ASP.NET core (i.e. web services), ConfigureAwait does nothing and is not recommended. I can confidently say that I don't want it in any of the code that I work on. We can make blanket statements, but clearly we do different things using different patterns and one size does not fit all.
https://stackoverflow.com/questions/42053135/configureawaitf...
.NET Framework is analogous to the JVM.
Micro-frameworks that don’t pack the kitchen sink like nodejs became popular, so Microsoft made .NET Core to be philosophically similar to that.
Because people such as yourself got confused by this, they eventually stopped pushing Framework and renamed .NET Core to .NET.
Microsoft has never been popular with the valley crowd, so everything they have ever done is pooped on constantly by people on here.
You should check out some of the posts on here when VS Code came out. A lot of people were amazed that Microsoft had built something decent “out of nowhere” like those people hadn’t just been living in a bubble their whole career.
Wouldn't CLR be closer to JVM? https://learn.microsoft.com/en-us/dotnet/standard/clr
And then... I don't know, .NET Framework to JDK, or something like that?
I like the whole thing (since .NET now runs on Linux well enough, C# feels okay as a language and has good tooling, like Visual Studio or JetBrains Rider) but there's definitely some naming and branding confusion sometimes going on, including in my own head.
No, its the library, not the runtime.
> Micro-frameworks that don’t pack the kitchen sink like nodejs became popular, so Microsoft made .NET Core to be philosophically similar to that.
.NET Core was still a very batteries-included library, it just dumped bits tied to Windows and made some other backward-compatibility sacrifices to get something cross platform and open sourcable.
> Because people such as yourself got confused by this, they eventually stopped pushing Framework and renamed .NET Core to .NET.
IIRC, they announced the long term plan to move all functionality that would be retained into Core and converge on a single implementation fairly early on, but to move forward in parallel until then .NET Standard versions were created to identify compatibility across Core and Framework during this period.) Dropping Framework and making Core just unqualified . NET (and eliminating the need for Standard) was just the completion of that strategy.
The JRE-equivalent in the .NET stack is the Common Language Runtime. The JRE processes Java bytecode; the CLR processes Common Intermediate Language (CIL, formerly MSIL) instructions.
Except you don't need anything .Net specific to be installed on the end user machine. I build and deploy single-file C# binaries to Linux all the time. There's no longer a dependency on having the .Net framework installed or having dozens of DLLs accompanying your binary.
Self-contained deployments are platform-specific.
A deployment that depends on a shared runtime can be cross-platform, since all the platform-specific parts are in that shared runtime installed for that machine's platform.
Which does .NET do? all of the above.
https://learn.microsoft.com/en-us/dotnet/core/deploying/
Digging deep into the core libs also makes you a better programmer by renforcing idiomatic patterns and exposing you to new techniques. Just the other day I dug into how ArgumentNullException.ThrowIfNull works, and discovered the newish (C# 10) attribute [CallerArgumentExpression("argument")] which gives you a string with the expression used to pass that parameter (i.e. in most cases, just the original variable name.) This has now entered my mental toolbox for stuff like validation code. The docs for this are actually quite good (https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...), but one has to know it's there. Often, reading other code is the best way to discover new features/techniques, and what better code to read than core .NET itself.
C# is an amazing, well thought out language, agreed.
> async/await is the same as in javascript
How many variations of async/await coroutine programming are there? Not saying I disagree, but I think most of them are similar.
> the documentation and official guides are miles ahead of Java
This is where you lose me. It's been a few years, but the last time I looked at dotnet docs it was a steaming pile compared to most other languages I've looked at, including Java.
The simplicity of:
Is the only reason I am still doing software for a living. I am completely over my tools. I just want to solve problems and build things now. If I have to lose 5% perf because I'm lazy, so be it. I'll hire someone to obsess over the 5% later on - and only if the customer complains.That being said, I would never tell am aspiring developer, especially one with a CS degree to learn it.
Besides, Microsoft, it’s not used heavily in most companies that pay top of the market and it’s definitely not the language that I would chose to use if I had to go through a generic coding interview.
Yes, I’m well aware that most of the 2.7 million developers in the US are “enterprise developers” and many use .Net. While .Net has been cross platform for years, it’s still the choice of mostly old school Windows shops.
Game dev was like a kind of inspirational boot camp for me. I got to see (for better or worse) what the true limits of a professional developer and artist were.
I couldn't even stand it for a year, but it shaped my attitude towards work and hard things ever since.
For example, very rarely in my experience does a function switch between synchronous and asynchronous. So when it does happen and I have to update upstream call sites, it's not so frequent nor painful enough to damn the whole abstraction and then go "see? it sucks".
When people suggest an alternative abstraction like Go, I compare my 50 lines of WaitGroup code in Go to things like `await Promise.all([prom1, prom2])` in Javascript. It's all just trade-offs.
Take a look at Java's approach (Loom + Futures + Structured Concurrency). It allows you to be succinct yet avoid the explicit async/await approach.
There's obviously a difference between launching an operation to run concurrently with the spawning code and launching an operation and awaiting its completion, but that difference does not require the introduction of an async/await feature, and is perfectly expressible with nothing but threads and synchronous code. In particular, there is no need for two separate syntactic domains between async and sync subroutines. Indeed, in Erlang, Go, and Java, all the same behaviour of async/await is achieved with no such need, and no need for different kinds of APIs.
Of course there is. The former constraints the type (or return-type) of the function it is used in, the latter does not.
> but that difference does not require the introduction of an async/await feature
I don't remember saying anything about async/await in my original post though.
That's not a semantic difference, but a syntactic one. Syntactic differences are expressed by which programs are accepted or rejected by the language; semantic differences are expressed by programs exhibiting different behaviours.
To show a semantic difference you need to find P and Q such that when the value of P is the same in the two programs, the value of Q could be different; that's what expresses the semantics of the code between P and Q. Again, there are some (e.g. P: `(t = currentThread(), true)`, Q: `currentThread() == t`), but they're purely technical.
> I don't remember saying anything about async/await in my original post though.
You said that the lack of need of a syntactic kind for asynchronous routines is a fallacy. It is not. Everything that you can describe with sync and async routines your can describe, with the same ease and semantics, with just sync routines and threads. There is no need for a special syntactic category of asynchronous subroutines.
I agree with that statement.
> semantic differences are expressed by programs exhibiting different behaviours.
It can cause different behaviour whether code runs on the same thread or on a different one (see one of my other posts here). That doesn't have to happen in every program and it doesn't have to happen on each execution, but that still makes it a different behaviour - at least in my terminology.
> You said that the lack of need of a syntactic kind for asynchronous routines is a fallacy. It is not.
That's not what I said (or at least not what I meant). I meant that it is doomed to fail to try to generally unify sync and async code on a language (or runtime) level.
What if you are reading the file and somewhere else in the code there is some logging happening. Should the logging flow stop until reading the file is done? Should it not? If not, what should happen if the "thing" (e.g. thread) that executes the code to read the file crashes?
Go does not treat sync and async the same. It specifically has syntax to deal with async, e.g. go routines.
I also found that startups (my preferred work env) tend to filter you out when you have too much of that on your resume which is super shitty IMO but it's a thing. I'd love to see that change. Until then I guess I'll stick with Kotlin and TypeScript.
It's either what you mentioned or they have very outdated development practices.
In my observation, more and more startups have been taking off with C# and .NET. It’s very productive and efficient. It truly shines in many popular benchmarks. With .NET, many potential choices have already been made for you, it’s a true framework. You get a great setup out of the box which you can tune to your liking. It’s like Angular. Java is like React - there’s a core library and the rest is on you, Spring to the rescue, or Quarkus, or Micronaut, or… a hundred other frameworks. Very flexible but also quite fragile. The CLR has much fewer knobs than HotSpot or OpenJ9. I like that actually. It’s just more efficient in most cases. Can’t speak to multi-terabyte heaps with CLR though, haven’t been there.
The GUI story with .NET is amazing. Other than MSFT’s libraries, you have Avalonia and Uno as popular alternatives. Avalonia is very snappy. Uno has a great potential. Both are multiplatform (handheld and desktop devices, with the same codebase). Then there’s Blazor for the web in various incarnations and MAUI on Xamarin. Kotlin’s (Jetpack) Compose Multiplattform is still a couple years behind Avalonia and perhaps Xamarin. And JavaFX GUIs have never been this good or resource efficient, there are a couple other Java frameworks but they are apparently financially too resource constrained to compete yet.
As for the enterprise, I’ve seen both, retrograde Windows shops reluctant to change, riding a slow-paced product lifecycle, as well as agile projects upgrading their .NET stack a few days after the most recent GA release, often having run CI on RC releases well ahead.
Perhaps it’s location dependent. Denmark being an outlier is virtually exclusively .NET. In China, Go is very popular, you also see many contributors to Go projects on GitHub from China. Same with Vue. In short, there are also socioeconomic idiosyncrasies to it.
This is not wrong.
.NET Framework = the built-in class libraries. Confusingly, also ".NET Framework" can mean the older, legacy, Windows only version of the whole thing.
ASP = the web server and web services centric parts of the framework.
NuGet = package management for built-in and 3rd party libraries.
Roslyn = the C# compiler
CIL : the bytecode "Intermediate Language" emitted by that compiler
CLR = Common Language Runtime for CIL, and related concerns such as GC and JIT
So I suppose "ecosystem" is correct.
C# a general purpose high-level language, OO-first but with some functional things.
The other problem is that it's broad: a person can use C# and.NET for one thing (e.g. Web services, or Desktop GUIs), and have strong opinions about what's needed and how to do things, and have them validated daily with all colleagues, assume that this is universal but ... those opinions are not true of the other.
Also the template support in the compiler was limited at the time. Later on there were other libraries that used templates like ATL and WTL.
You did get a lot for free from MFC when implementing document editing applications for OLE support, but that was an increasingly small portion of development.
I think Borland went a different way with compiler extensions for producing a Win32 application with their OWL library.
With async/await the code ends up structured the same as if the operations were synchronous. It's much easier to follow and modify.
The vast majority of projects don't use CGo, and of those that do, the majority doesn't call CGo in a tight loop, making the performance impact mostly irrelevant.
So it's really coming back to the performance-orientedness of a language.
.NET is the opposite - it was designed from the get-go from cross-language and cross-runtime interop. Back when Java folk still had to struggle with JNI to use anything native, .NET 1.0 had P/Invoke and COM Interop. And now that it has async/await, it's all mapped down to a C-compatible ABI for interop purposes, so you can easily write async code in C# that calls into async code written in C++ that calls back into C# etc.
Good: Rust, JavaScript/TypeScript, C# and Swift
Interesting: Golang, Erlang/Elixir
Bad: Java and most JVM implementations, C++ (cumbersome, the language is bad by modern standards in general)
https://www.sitecore.com/landing/uk/2023/the-future-is-compo...
Most of the new acquisitions are based on Java, with exception of Order Cloud and Content Hub.
.NET Core is available for XP headless rendering SDK, however it is NextJS/JSS that are getting the spotlight and there is plenty of DIY if you will go down .NET Core, specially after the cooperation agreement with Vercel.
Classical XP is stuck on .NET Framework and XM Cloud customisation points favours WebHooks instead of writing .NET code.
CDP, Search, Send are low code tooling, only exposing Web APIs, and JavaScript.
Content Hub does allow for some use of C# in scripting, but it is mostly about Webhooks as well.
https://doc.sitecore.com/xp/en/developers/hd/210/sitecore-he...
However as you can see, XM Cloud clearly favours JavaScript, specially in the components that replace content editor and page graphical editing,
https://doc.sitecore.com/xmc/en/developers/xm-cloud/sitecore...
And then check the acquisitions, https://doc.sitecore.com/
Task.async/1 (or Task.async/3) at a high level just spawns another BEAM process links it to the currently running process, sets up a process monitor, and then runs. Task.await/2 just pulls from the mailbox of the current process for a returned message.
There is a semantic difference between just running a line of code or running ith with Task.async right? So then there is a distinct concept of sync vs async.
Task.async/3 and Task.await/2 are, from the thousand foot view, the equivalent of:
In Elixir/Erlang parlance, I have just spawned a thread where the BEAM VM handles both the internal data structures to facilitate concurrent-safe communications between threads and the scheduling in userspace.Put more generally, the mentality of concurrency in Elixir/Erlang is the equivalent as that of traditional pthreads in C.
Is it (except for performance) always 100% equivalent whether a calculation is done "locally" or whether it is ran on some other, potentially just spawned, thread and then retrieved from this thread?
If your answer is "no it is not always 100% equivalent" then we agree (and I think we do).
An Elixir, Erlang, or any other developer using the BEAM VM (or at the very least myself since I don't want to generalize) doesn't make a semantic distinction between spawn and Task.async/3 because there is none. The significant differentiation is whether or not the caller is expecting a returned value, if not then Task.start_link can be used instead.
I don't need to worry about function colouring because there is none. I also don't need to worry about async vs Task.run like in C# because in Elixir there is none. To be asynchronous in Elixir/Erlang/etc... is to be synchronous. To equate it to modern backends, we basically have a microservice but every service can directly call each other.
I think I also might have been talking around you a bit, the BEAM VM sets itself apart from C#'s async/await or the TPL due to its implementation of the actor model. This video by Sasa Juric can explain better than I can the general overview of the BEAM: [1].
But a rough tl;dw, a BEAM process is to the BEAM, what an OS process is to the OS. We don't define a main function as the process supervisor in BEAM spawns processes we define instead and those are our "entrypoints."
[1] https://www.youtube.com/watch?v=JvBT4XBdoUE
Sure, but that was not my question. 100% equivalent means that there cannot be any observable change in the behaviour of the whole program/application (except for performance).
Or in other words: is the sole purpose of the existance of `Thread.async` (or spawn, pick whichever you want) to change performance characteristics? If not, what is the purpose of its existence then?
In a vacuum (i.e. via spawn/1 or spawn/3) there is no observable change in behaviour throughout the whole system.
If they're linked via spawn_link then a child process crashing means the parent process dies with it unless handled in some way.
So ultimately, no there isn't observable changes. My logging process doesn't care or even know that my WebSocket process crashed.
> Or in other words: is the sole purpose of the existance of `Thread.async` (or spawn, pick whichever you want) to change performance characteristics? If not, what is the purpose of its existence then?
As stated before Task.async/3 is just a convenience wrapper around low-level BEAM primitives, there isn't anything special about Task.async/3 that you couldn't do via spawn.
The reason being that the BEAM VM schedulers prioritizes overall latency of the system over throughput by enforcing reduction calls (parlance for a function call, but specifically for tracking looping in the BEAM) at around 4000 calls, then it moves the current process back into the queue; repeat ad infinitum.
So you don't get massive speedups just spawning a new BEAM process but your independent units of execution still enjoy the same overall latency as before. If you want performance increases in the BEAM you would need to jump to dirty schedulers and NIFs which carry their own dangers.
Okay, let me follow up on this one. So could we take arbitrary Erlang/Elixir programs and automatically/mechanically rewrite them to push previously inlined calculations to be run on a different process (using spawn) or the otherway around - and that without causing any observable difference in behaviour in any situation except for difference in performance?
> As stated before Task.async/3 is just a convenience wrapper around low-level BEAM primitives, there isn't anything special about Task.async/3 that you couldn't do via spawn.
You can likewise answer my question for the BEAM primitives (like processes/threads): is the sole purpose of the existance of those primitives to change performance characteristics? If not, what is the purpose of its existence then?
Also, just so that we understand each other: I'm a big fan of the BEAM. This is not a criticism or anything; I just want to explain why I believe that function coloring (or however you call it) can never be conceptionally be removed from a language if sync/async is to be supported.
We probably should've established this way earlier, sorry if I came across as pretentious or off putting.
I'll answer in a bit of reverse order.
> I just want to explain why I believe that function coloring (or however you call it) can never be conceptionally be removed from a language if sync/async is to be supported.
To respond in more of a roundabout way, why doesn't C have colouring issues if it too supports asynchronicity (I'm pretty sure I've made up a word but hope the point gets accross fine) via pthreads?
The colouring issue is, at least under my understanding of C#'s async/await system is to accommodate for the fact that Rosyln transforms the C# source into .NET IL with a state machine. It's, to me at least, just a consequence from what layer of the stack we talk at. Erlang doesn't need to worry about since the runtime was designed to run with this specific model of concurrency in mind; but if we look at C#, .NET only supports using OS threads (though David Fowler is investigating green threads in the, iirc, labs repository on the DotNet Github) the async/await is, for lack of a more better word, bolted on. It's to my understanding that IronPython (or maybe it was PythonNet) has difficulties calling into C# async for that reason; though if I'm wrong please correct me.
> is the sole purpose of the existance of those primitives to change performance characteristics? If not, what is the purpose of its existence then?
Fault tolerance, latency, scalability, perhaps minor throughput increases (yeah this is a bit of a contradiction to what I said earlier, but I've revised my opinion that you wouldn't see massive performance gains unless resorting to NIFs).
The Phoenix Framework's 2 million websocket connection challenge I think is a best demonstrator for the use case of BEAM processes. A specific websocket connection dies? No problem, the Supervisor respawns it. Two million connections receiving a Wikipedia length article on a topic without the entire system coming to a crawl? No problem. There isn't a need to worry about kqueue, poll/epoll, or IOCP here, just fire off a task for the purpose and let it do it's thing.
But overall you wouldn't spawn a BEAM process just so you could compute the matrices behind RAID6/RAIDZ2, you'd delegate those to NIFs. The biggest gains in overall performance came from BeamAsm instead.
> Okay, let me follow up on this one. So could we take arbitrary Erlang/Elixir programs and automatically/mechanically rewrite them to push previously inlined calculations to be run on a different process (using spawn) or the otherway around - and that without causing any observable difference in behaviour in any situation except for difference in performance?
Yes. Using the case of the 2 million websocket connection challenge earlier, it doesn't really matter if each of those connections spawned another process as part of its routines, the other processes don't know and don't care. Taking a more generic case of Elixir's GenServers (or gen_server in Erlang), when I perform a call (as in the GenServer behaviour) it doesn't matter to the calling process what happens behind the scenes, it just blocks and waits for a response. The GenServer could fire off any number of processes it wants but the calling process doesn't care about that.
But it does have those issues. In fact it is worse than in most other languages - just because there is no distinct support of async in a language does not mean it doesn't have async capabilities. C does have those, you just don't have any language support whatsoever, you have to take care yourself. Delegating the execution to the OS doesn't change that.
For example, if you want to transform elements in an array concurrently, you now have to use pthreads. You cannot do that while using the same code as before (which is would removing function coloring is all about).
Maybe we have a misunderstanding here: I'm talking abouyt sync/async not the async/await syntax that many languages have. The latter is to help deal with colored functions and the former is what enables the existance of concurrency. Any language that is - in one way or the other - capable of running code concurrently is suffering from that problem. It can be avoided by enforcing synchronous execution only, which some languages do (especially some DSLs), but most general purpose languages support concurrency.
And once they do, there's two choices: the programmer has to deal with it (using Thread.async, spawn, pthreads, go channels, promises, ...) or they don't. If they don't then sync and async code must look exactly the same (or be able to be converted forth and back automatically). And this goal (either making it look the same or converting it automatically) is just not solvable in general. It has been tried for decades.
For instance, check the paper "A Critique of the Remote Procedure Call Paradigm" from 1988. I think it was from Tanenbaum. It lists some of the problems that just cannot be solved in general. Some programs will always behave different after the automatic conversion, it's impossible to prevent that.
> > Okay, let me follow up on this one. So could we take arbitrary Erlang/Elixir programs and automatically/mechanically rewrite them to push previously inlined calculations to be run on a different process (using spawn) or the otherway around - and that without causing any observable difference in behaviour in any situation except for difference in performance?
> Yes.
What I'm trying to explain is that you are mistaken. It just is not possible. Maybe in specific cases, sure, but not in a safe way for every usecase that can be thought of.
And neither Erlang nor Elixir (nor the latest BEAM language, I forgot the name), luckily, try to do that. They give this controller to the developer and let them deal with the decision of how and when to use concurrency. They just try to make that as easy as possible, but they don't try to make sync and async code look and be the same.
Moreover, there is a context switch and it's more likely the code will run on a different core if you run it async than if you run it in the same process (which is very likely but not guaranteed) to run on the same core.
The way in which they are equivalent, is that the code that you write is identical, and the bytecode that gets run, and the existence of implicit yields is identical between the async and "sync" code.
> Moreover, there is a context switch and it's more likely the code will run on a different core if you run it async than if you run it in the same process (which is very likely but not guaranteed) to run on the same core.
Yes I agree, but the question as presented by valenterry asks us whether or not there is some existing semantic difference and not under the confines of performance (both gains or losses). Regardless what you have stated are all true.
But as an aside, and not directed towards you, Task.async/3 doesn't do anything that the developer cannot already do. Even in the tutorial for the learning Elixir, the fledgling developer is exposed to the different mechanisms that power Task.async/3, the source code [1] reflects this, although supervisors are covered much later down the line. The documentation for
> The way in which they are equivalent, is that the code that you write is identical, and the bytecode that gets run, and the existence of implicit yields is identical between the async and "sync" code.
And just to add on for those not familiar with the BIFs, receive, which is what Task.await/2 and Task.yield/2 use under the hood, yields execution. NIFs are another one.
[1] https://github.com/elixir-lang/elixir/blob/fb729784e5504f499...
IOW, async/await in the BEAM is a high level thing (like goroutines) which is one of two reasonable choices, the other reasonable choice being "what zig did".