I agree, I don't tend to think they are quite as dangerous as GOTOs have proven to be, but I do understand the criticism. The example provided about their use in interrupt handlers is of particular relevance.
Actually I have to apologize to the GOTO somewhat. Semaphores are worse than GOTO.
Dijkstra didn't understand that GOTO is equivalent to tail recursion. The way you untangle the spaghetti of a GOTO graph with shared variables is to divide it into basic blocks, and turn them into pure tail-calling functions. The assignments to shared variables become argument passing. From there you might be able to identify some meaning in those functions.
The GOTO is also a fundamental block in that you can use conditionals and goto as a target language for higher level constructs --- and this is clear and efficient.
Semaphore API calls are a horrible target language for implementing other synchronization primitives. They are too encapsulated: each semaphore is a tiny mutex protecting only a counter. So then you face an abstraction inversion right away: to protect anything else, you're using a tiny mutex which protects a counter, plus that counter, as a bigger mutex.
I currently work in a codebase which carries its won semaphore, which is used a lot. The implementation is a POSIX mutex, condition variable and counter! Ouch, that hurts. It's more regressive than a tax that takes money from single moms to give a corporation a break.
goto isn't that dangerous, or that strange. It has it's place (like inline retries instead of recursion or for-loops) but it's rare to reach for them in most languages.
Semaphores are like a restricted form of goto which lets you branch exactly, say, two lines forward or two lines back. Building concurrency primitives out of semaphores is like using that goto to build control flow.
The time has shown that anti goto cult was huge overreaction that gained size due to decades long brain washing by academics on inexperienced programming practicioners (students)
Goto in newer languages is not bad and exceptions which arent as criticized are way more powerful in goto-like sense
Original gotos from that infamous paper were more powerful
In fact you can. In Common Lisp, (go ...) is a dynamic control transfer that performs unwinding and can jump out of a lambda. This is similar to what blub languages call exception handling.
(tagbody
10
(format t "label 10~%")
(unwind-protect
(funcall (lambda () (go 10)))
(format t "unwinding!~%")))
Output:
label 10
unwinding!
label 10
unwinding!
...
How it works is that (go 10) identifies the surrounding tagbody as an exit point for the exception-like dynamic control transfer. That (go 10) is occurring in a sub-form of tagbody. That entire sub-form is abandoned, with unwinding, and then tagbody catches that control transfer, like an exception handler. tagbody then switches control to the desired label. Effectively, the exception-like control transfer is accompanied by a piece of datum: the label.
tagbody requires lexical visiblity between the go and tagbody; though the label bindings and the control transfer are dynamic, they have to be physically enclosed. That allows compilers to optimize tagbody more. A Common lisp compiler doesn't have to suspect that function (foo) can branch to the label bar in (tagbody (foo) bar) and can conclude that the label is unused, and the tagbody can be entirely optimized away. However, cross-function go does work, as the lambda shows in my example.
Dijkstra didn't understand that GOTO is equivalent to tail recursion. The way you untangle the spaghetti of a GOTO graph with shared variables is to divide it into basic blocks, and turn them into pure tail-calling functions. The assignments to shared variables become argument passing. From there you might be able to identify some meaning in those functions.
The GOTO is also a fundamental block in that you can use conditionals and goto as a target language for higher level constructs --- and this is clear and efficient.
Semaphore API calls are a horrible target language for implementing other synchronization primitives. They are too encapsulated: each semaphore is a tiny mutex protecting only a counter. So then you face an abstraction inversion right away: to protect anything else, you're using a tiny mutex which protects a counter, plus that counter, as a bigger mutex.
I currently work in a codebase which carries its won semaphore, which is used a lot. The implementation is a POSIX mutex, condition variable and counter! Ouch, that hurts. It's more regressive than a tax that takes money from single moms to give a corporation a break.