If I’m using threads for each of my tasks, then why do I need async at all? I find mixing async and threads is messy, because it’s hard to take a lock in async code, as that blocks other async code from running. I’m sure this can be done well, but I failed when I tried.
It's messy in something like Python, but mostly transparent in C#. In C# you effectively are using async/await to provide M-N threading ala. GoLang, it's just different syntax.
OP references the C# method Task.WhenAll so they might be assuming other languages are equally capable.