Goroutines and channels are not bad at all, but I feel there's some part missing to use them effectively without too much headaches. I've often found sync.WaitGroup and mutexes far more useful than channels.
Actually, I wrote an entire article about this, discussed here: https://news.ycombinator.com/item?id=26220693
It's a real problem because of 3rd party libraries. Too many 3rd party libraries produce deadlocks in unusual scenarios, and it can take almost as much time to write the library on your own than to find out whether their use of goroutines is correct. Deadlocks are really everywhere. The same with sync.atomic by the way, the use of these primitives is rarely correct and people keep using them despite the warnings in the docs. I'm not claiming to be smarter, on the contrary, the problem really is that Go's choice of concurrency primitives is only suitable for concurrency experts and most programmers (including myself) aren't.
I was under the impression that Go had first party tooling for deadlock checking, something called threadanalyzer if I recall correctly.
Did any language actually do that before rust?
It's also somewhat less useful without compiler-checked ownership as you can easily have the protected data escape the lock.
1. https://www.type-driven.org.uk/edwinb/papers/fi-cbc.pdf (free; 2009)
2. https://livebook.manning.com/book/type-driven-development-wi... (registration wall; 2017)
3. https://research-repository.st-andrews.ac.uk/handle/10023/23... (free; 2021)
Only if you’re also concurrency free.
If you can still have two actors waiting on one another, you’re letter-of-the-law deadlock-free, but your program is still in effect deadlocked.
> the interesting bit is being data-race free:
Data race freedom is much weaker (and easier) than deadlock freedom, or race condition freedom.
Safe rust is free of data races for instance. But still subject to deadlocks, and other race conditions.
I'm not sure we're using the same definitions. If you're non-blocking you have system wide progress, I'm not sure your examples of two actors mutually waiting on each other qualify.
In any case I think Pony qualifies as wait-free too, which avoids this scenario. Of course one can still have an infinite for loop, or an infinite ping-pong between actors.
Of course it qualifies, that's a classical text book example of deadlock.
Can anything really shield you from that? It seems nothing can, in the same way nothing can really shield you from an infinite loop.
If they are blocked it is not a live lock. If they are spinning in a try_read then it would be a live lock.
> Can anything really shield you from that? It seems nothing can, in the same way nothing can really shield you from an infinite loop
Non-Turing Complete languages can be proven to terminate(they do exist, see total functional programming). Similarly I think there are programming paradigms that might be truly deadlock free but my recollection is fuzzy. IIRC writes must never block and reads must handle all blocking conditions with guarded commands. I think you can still livelock (by failing to do something when woken up for example).
If they're blocked then we are no longer talking about non-blocking, did we go full circle?
> Non-Turing Complete languages can be proven to terminate [...]
Of course, and while I appreciate the point for sake of argument, these languages have very limited applicability.
> Similarly I think there are programming paradigms that might be truly deadlock free but my recollection is fuzzy.
Look up Pony, from an earlier message in this thread :)
after 5 minutes of googling, pony has non-blocking reads and uses promises with continuations. The CPS transform of a program that can deadlock will still deadlock. Just because you reify your wait queue does not mean you are free from deadlocks. They are just much harder to debug as you do not have a clean stackframe and process identity.
edit: to be more clear, to say that a program that uses promises and continuations can't deadlock is equivalent to saying that a C program running on linux can't deadlock because the kernel is still able to make progress. From the kernel point of view the C program looks exactly like the pony program, with continuations suspended on wait queues.
More in this presentation[0] (slides here [1]) and this [2].
[0] https://www.infoq.com/presentations/pony-type-system/
[1] https://qconlondon.com/london-2017/system/files/presentation...
[2] https://www.infoq.com/podcasts/sylvan-clebsch-pony-formal-ve...
[1] https://kevinhoffman.medium.com/modeling-non-blocking-intera...
while true:
x = read s1
write s2, x
y = read s2
write s1, y
This can deadlock. If for example s1 stops sending data, read(s1) blocks, so the proxy won't forward any data from s2 back to s1, potentially leading to a full pipeline deadlock.Now, the above can be expressed in purely non blocking fashion as follow :
letrec f = async_read s1, \x ->
async_write s2, x \->
async_read s2, \y ->
async_write s1, y, f
It still deadlocks exactly as the previous program.I'm not going to learn pony for the sake of an example, but I'm 97.5% sure you can express the above in Pony, except you do not pass the continuation directly to read and writes but to the returned promise. Sure, the pony runtime will still work and technically the actor could potentially handle other messages, but that's not very useful for the end user that has to deal with a deadlocked pipeline.
So claiming that pony is deadlock-free might be true in a trivial sense, but in practice it is at the very least misleading and potentially dangerous.
edit: it is of course possible to write the program in such a way that it doesn't deadlock, the point is that if pony was truly deadlock free it would reject all programs that can deadlock.
edit2: and of course, if one were to show that the above program is in fact not expressible in pony, it would be a great result and I would want to know about!
The scheduler wakes up actors that have messages (in their local/private queue) to process, all actors can do is to consume their queue and send (wait-free) messages to other actors if they want to.
From your example "read X" would be reading from the actor's queue, so it would not be blocking. If the message queue is empty that actor won't be scheduled, no blocking there.
That "while true" would not be writeable as such, you'd have to have two "behaviors" (in Pony terms, you can imagine it's an event handler) one that process the writing of s1 and one for the writing of s2, and "on read" they'd do the write. There'd be no "while true".
> I'm not going to learn pony for the sake of an example ... > edit2: and of course, if one were to show that the above program is in fact not expressible in pony, it would be a great result and I would want to know about!
I might have misunderstood what your example was supposed to do, but I suspect you'd find it interesting to try it out.
Fwiw that’s possible with dependent typing, the compiler can require a proof of progress (and thus termination).
There's no way to actively wait for another actor (except for using callbacks). The only way for your situation to occur would be if one of the actors receives a message with an included callback, but decides to never call it.
How is it letter-of-the-law deadlock-free?
I expect you have to go out of your way to make it happen in erlang and actor based systems
Channels are vastly oversold given their actual suitability and worse still, add their own classes of race conditions (deadlocks) so you often fallback to using mutexes for those, hopefully rare, occasions you need to pass a shared stated. And once you're reliant on mutexes then you're really no better off than you were with any other language where threading is an afterthought.
The only real benefit Go brings is the ease of spinning new goroutines, but that can be a double edged sword if you don't understand how to manage them in the first place, and the green thread model making goroutines lightweight. Though even this isn't exactly unique to Go either.
I feel like this is one of Go's biggest weaknesses yet it seldom seems to get the same discussion generics and error handling (both of which are annoyances in specific edge cases but neither of which cause the same level of show stopper bugs).