3 ms·
Kind of sick of seeing this misconception so I want to clear it up: async/await (as most commonly implemented) != pre-emptive and truly parallel multithreading.
by bruce343434 2y ago
Kind of sick of seeing this misconception so I want to clear it up: async/await (as most commonly implemented) != pre-emptive and truly parallel multithreading.
All parallel execution is asynchronous, but not all asynchronous execution is parallel.
async/await interleaves execution in a cooperative fashion: a "fiber" runs until it yields by doing something blocking (such as blocking I/O, the most often cited use case). There are no data races and no need to use synchronization primitives since there is never a case where 2 fibers modify a piece of data at the same time: because execution is interleaved, the operations are always ordered one after the other. Whenever a fiber blocks, execution is returned to the scheduler, which resumes execution in one of the other fibers which were blocked.
fork(pthread_create)/join(pthread_join) parallelizes execution: a "thread" runs until it is done or is killed externally (the scheduler can also pause execution). A thread X can wait for another thread Y to be done by calling join(Y). There is a need to use atomics or other synchronization primitives such as channels (or mutexes, or barriers, or semaphores) when 2 threads share a variable in order to make sure only 1 at a time modifies, because although threads can be ran in an interleaving fashion (and sometimes they are interrupted (pre-empted)), they are usually ran in parallel on different CPU cores/threads (if you have a SMT CPU, which most nowadays are: n CPU cores and each CPU core has 2 "CPU threads").
P.S. A CPU thread != OS thread
- hot_gril 2y agoasync/await is just syntax. There's nothing saying that two async calls can't run fully in parallel. "Blocking" is subjective too; in a way even JS is doing some parallelism.
- bruce343434 2y agoI covered my ass: > async/await (as most commonly implemented) Javascript is famously single threaded, which is probably the runtime on which most async/await code runs today. But yes, Rust for instance has the Tokio async runtime which is parallel, although it also has cooperative async runtimes. For those who are reading this and feeling unsure: Go has parallel execution, and it calls `fork()` `go`. Go doesn't feature a `join()`, for that you must use channels: func main() { done := make(chan bool) go func() { fmt.Println("goroutine running") time.Sleep(2 * time.Second) // simulate some work done <- true }() <-done fmt.Println("goroutine finished") } Funny thing: if you have a goroutine which is waiting to receive some value from a channel, but it never does this, and the other handles to the channel are gone, you get a goroutine leak! The goroutine is blocked, and unrecoverable/unkillable.
- kfajdsl 2y agoI mean, the top comment wasn't talking about runtime characteristics, they were talking about the patterns resulting from using co-routines with messaging vs async/await.
- deleted 2y ago[deleted]
- hot_gril 2y agoThat's right. So if Golang wanted to do async/await, it would probably be in a fully parallel way.
- fmbb 2y agoIsn’t a WaitGroup a more common way to do join? https://pkg.go.dev/sync#WaitGroup https://pkg.go.dev/sync#WaitGroup
- neonsunset 2y agoThank you, this is indeed true, though it's important to be aware of implementation differences - poll-based Futures that are "merged" into a single big Task like Rust, or continuation-scheduling granular hot-started Tasks like C#. e.g. in C#, the following is true: using var http = new HttpClient() { BaseAddress = new("https://news.ycombinator.com/") }; // Runs sequentially var page1 = await http.GetStringAsync("?p=1"); var page2 = await http.GetStringAsync("?p=2"); // Submitted sequentially, everything after the first yield completes in parallel, IO does not block the caller var page3 = http.GetStringAsync("?p=3"); var page4 = http.GetStringAsync("?p=4"); // Waits for page3, then page4 to complete, they are effectively parallel DoSomething(page1, page2, await page3, await page4); But in Rust, this requires explicit join handle, and if you would like to parallelize/fork the execution flow - you have to schedule a separate task. It requires more effort but in return you get lower async overhead, which is critical for areas Rust is applied to (and host-starting Tasks or Futures would not be easily compatible with borrow checker I presume).
- sureglymop 2y agoYou said "All parallel execution is asynchronous, but not all asynchronous execution is parallel." But later you mentioned threads running in parallel on different CPU cores. The way I always understood it, is that this is the only real form of parallelism able to happen on a CPU. But that does seem kind of incompatible with your statement at the beginning.
- dekhn 2y agoThe way I think about it is, when Rob Pike says "Concurrency is not parallelism", I really rewrite it as "All concurrency is parallelism, but not all parallelism is concurrency." If you just think of concurrency as an instance of parallelism, you start to realize there are many ways to achieve concurrency, and thread-safe queues are one of the better ways to do it. Individual CPU cores are parallel because they are executing multiple instructions simultaneously (even though they are from the same instruction stream).
- yencabulator 2y ago> "All concurrency is parallelism, but not all parallelism is concurrency." The other way around. Alternating two executions on one CPU core is concurrency, but not parallelism.
- throwaway894345 2y ago> All concurrency is parallelism, but not all parallelism is concurrency As sibling notes, you have it exactly backwards. It should read "All parallelism is concurrency, but not all concurrency is parallelism." I thought maybe this was a typo, but then you said: > If you just think of concurrency as an instance of parallelism, you start to realize there are many ways to achieve concurrency, and thread-safe queues are one of the better ways to do it. This is demonstrating parallelism as an instance of concurrency, not the other way around. A thread-safe queue can be processed by multiple tasks running interleaved in a single thread in which case it is executing concurrently, but not in parallel. > Individual CPU cores are parallel because they are executing multiple instructions simultaneously This is an implementation detail of the CPU. Not all CPUs execute instructions in parallel.