"While I'm on the topic of concurrency I should mention my far too brief chat with Doug Lea. He commented that multi-threaded Java these days far outperforms C, due to the memory management and a garbage collector. If I recall correctly he said "only 12 times faster than C means you haven't started optimizing"." - Martin Fowler https://martinfowler.com/bliki/OOPSLA2005.html
Go has both memory management and a quite performant garbage collector, as you probably know.
Your article is from 2005, so I take it to mean that Java 6 was doing the interesting things you talk about - from my recollection, the go-to at that time was java.util.concurrent. (I haven't kept up with Java, so it could still be state-of-the-art.)
https://www.sciencedirect.com/science/article/pii/S016764230... - this paper describes the implementation as "The heart of the framework is maintenance of queues of blocked threads, which are restricted here to FIFO queues." If you're not familiar, this is exactly what go channels are, just baked into the language rather than in the standard library.
The major differences being that java.util.concurrent in Java 16 is a bit more powerful as the one in Java 6, Java doesn't have one single GC rather multiple vendor implementations to choose from, with virtual threads on the horizon.
Additionally one benefit over Go, is that in Java (likewise in .NET), the scheduler algorithm is replaceable.
That's valid, I didn't mean to imply that Java was stuck in 2005. I'm having a hard time following the clues that bullen is trying to leave in order to divine their meaning, so I was trying to limit the scope.
I don't see the problem here. You need to have serial access to the counter. You could use a single-threaded webserver but there is a lot more to that request than actually incrementing the counter. It still makes sense in most cases to use a threaded webserver and lock on incrementing the counter. (Especially if you are using a concurrent hashtable so that you can increment multiple counters in parallel)
You could use the atomic package and forego the lock. You can also have a go routine with a channel that receives the counter messages. No need for a lock there.
But use it if you really need it. Likely that’s not your bottleneck.
I am not a Go expert so please correct me if I'm wrong, but... The fact that the stdlib HTTP utils in Go recover from panics in handlers and provide no way of changing this via easy setting is one of the things that really annoy me. Seemingly, they consider it a backwards compatibility break since it's a behavior change that would affect all existing middleware you try to use (see e.g. https://github.com/golang/go/issues/16542). You need to provide your own error handling middleware (of which several exist).
A beginner needs to be very careful and read the docs to catch e.g. this line:
"If ServeHTTP panics, the server (the caller of ServeHTTP) assumes that the effect of the panic was isolated to the active request. It recovers the panic, logs a stack trace to the server error log, and either closes the network connection or sends an HTTP/2 RST_STREAM, depending on the HTTP protocol. To abort a handler so the client sees an interrupted response but the server doesn't log an error, panic with the value ErrAbortHandler. "
I think that is an unintuitive assumption that makes for brittle software. Also, the fact that Go kind of blesses the use of panics internally in a library (e.g. "The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values.", https://blog.golang.org/defer-panic-and-recover) makes for even more brittle software, as predicting what state a control flow interruption like panic through a call stack leaves your data in can be challenging and require great care to avoid invariants being violated. Last I checked, e.g. Rust did not allow the reuse of data owned by a panicking call stack without explicitly asserting that it is still considered valid.
I guess I'm showing my bias for languages that make it harder to make mistakes, but I don't like brittle stuff like encouraging panic-like control flow for non-exceptional situations, implicit reuse of "panicked data", missing non-exhaustive match on enums, missing enums altogether, missing sum types and pattern matching making everything from detailed error management to proper state representation more brittle, etc etc. I'll stop there and not get into the rest of the stuff.
Go is one of the languages my employer pays me to write and I will say that I have a significantly higher appreciation for it now than when I started (it IS very beginner friendly and very ergonomic in general), but I wish it would help me more to write really robust and correct software.
I would offer the same criticism of them, as it's the behavior in general that I don't prefer. But of course it's perfectly fine to disagree, this is all about preferences. I like fail fast, being required to handle errors and my tools (including programming languages) to very clearly help me identify where errors can occur.
This was actually more challenging to respond to than I thought it would be.
In Go (as the blog post that I linked alludes to): "The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values."
However, it is not the expectation that libraries should recover from panics they didn't start themselves. If they did, it would be very hard to panic and actually have a natural expectation that your application would exit due to that panic, which is how most Go code behaves and expects to behave.
As I wrote in another reply, neither the main goroutine/function nor spawned goroutines automatically recover from panics. They DO shut down the entire server if any code in them panics (provided that no boiler plate recovery is performed at the root of the call stack, which in itself would make it very, very hard to reason about the consistency of the data that might have been touched before panicking at any one of countless of operations in the code in the call stack).
Therefore, one might also argue: Should the entire server shutdown if any worker thread causes a panic? I do agree that it is more plausible for an HTTP request thread to do so, but not enough to change such a basic behavior. Go doesn't allow to register a global panic handler to be able to perhaps recover but also log panics in a consistent way, such that it would be applied consistently across your entire process and customizable to the preferences of the developer as to their chosen trade off between "fail fast/never continuing processing in the face of unexpected programming errors" and "an unexpected programming error occurred but I still want to continue executing and hope that nothing broke in my application".
And I do acknowledge that different developers/organizations would want to make that trade off differently, but at present it is not very convenient to do consistently. The Go creators chose not to allow global panic handles (there are a bunch of discussions about it on Google Groups and similar, and I do agree with some of the arguments in them).
Some people (myself included) might prefer that the application fails and whatever orchestration manages this application triggers an alarm with operations staff, developers, etc, without instead risking that an application keeps running and perhaps due to some invariant now being violated and data inconsistent keeps making mistakes, perhaps serious mistakes.
This of course depends a lot on what kind of application you're building and how important this is, how much uptime for partial (but potentially buggy) functionality weighs against never risking serious mistakes. I tend to think that the correctness of most software in the world is actually important these days, but I fully admit there's a scale. Go however is being used to build all kinds of software these days.
If one doesn't like that strategy, and wants to build software that recovers in other fashions then perhaps one should have a look at Erlang and its process supervisor trees, or other systems with other trade offs.
It is a genuinely hard question, I admit that. I just don't think Go's stdlib in this case chooses a position on that trade off that I like, that's all. It's all opinion, and we're all entitled to them. Thanks for asking, and forcing me to put thoughts into words!
> The fact that the stdlib HTTP utils in Go recover from panics in handlers and provide no way of changing this
It's comically easy to just wrap the root handler, catch panics yourself, and explode.
> You need to provide your own error handling middleware (of which several exist).
And you need to provide your own handlers anyways, which seems reasonable to me, unless you're against writing code?
> A beginner needs to be very careful and read the docs to catch e.g. this line:
Yes you need to read the docs to understand what a function does. This does not require being very careful where I'm from.
> Also, the fact that Go kind of blesses the use of panics internally in a library ... makes for even more brittle software, as predicting what state a control flow interruption like panic through a call stack leaves your data in can be challenging and require great care to avoid invariants being violated.
They make this pretty clear, that you shouldn't leak the panic, in which case it's just an implementation detail. If your library is brittle, that's on the implementer. You don't have to use panics for control flow. It's just something you can do. A tool. Sure, maybe it's sharp.
> I don't like brittle stuff like encouraging panic-like control flow for non-exceptional situations
I've never seen anything remotely encouraging use of panic for control flow. The blog post even states it's uncommon and unusual. I don't know how you interpreted that blog post, which reads to me as "here's how defer works", to being "you should use panic for control flow whenever possible."
I'm not a native English speaker, so I had to look up a definition of disingenuous to make sure I didn't misunderstand you. I promise you I'm trying to be candid with my own opinion, and not trying to deceive in any way. What would my nefarious purpose be? I'm just stating my opinion. Whether something is brittle or not is an opinion within a range to me, not an absolute. Perhaps my English is just poor. I'm sorry if you genuinely feel I was being disingenuous.
> It's comically easy to just wrap the root handler, catch panics yourself, and explode.
> And you need to provide your own handlers anyways, which seems reasonable to me, unless you're against writing code?
> Yes you need to read the docs to understand what a function does. This does not require being very careful where I'm from.
Yes, it's "comically" easy to do so if you know that you need to do it. I do it. I didn't say it was hard to do. But Go prides itself on being beginner friendly, having consistent behaviors and not having many ways of doing the same thing. The main function doesn't automatically recover from panics. Spawned goroutines don't automatically recover from panics. I feel like it wouldn't be outrageous to consider this an expected behavior for someone writing Go code, as very few libraries to my knowledge recover panics they didn't start themselves. The behavior of the stdlib HTTP utils diverge from that expected behavior. I think there are tons of developers in every language who don't necessarily scour the docs but assume consistency with some behaviors that seem basic and logical. Again, opinion, if that wasn't clear.
> They make this pretty clear, that you shouldn't leak the panic, in which case it's just an implementation detail. If your library is brittle, that's on the implementer. You don't have to use panics for control flow. It's just something you can do. A tool. Sure, maybe it's sharp.
Yes, they make it clear how it should be used if it is used, and I didn't claim otherwise. I also didn't claim that leaking it was the problem, but instead that maintaining invariants in the data of the library using the mechanism can be a hard thing to do, and whenever something is hard to do right it can contribute to brittle software.
> I've never seen anything remotely encouraging use of panic for control flow. The blog post even states it's uncommon and unusual. I don't know how you interpreted that blog post, which reads to me as "here's how defer works", to being "you should use panic for control flow whenever possible."
Yes, I will meet you half way here. :) This particular blog post doesn't encourage it, and at the moment I can't other any of the other places I've seen it. The blog post gives an example of Go stdlib using it, and then describes how to use it in a library if it is used. My personal preference would be to offer more caution or perhaps even discouraging in that blog post, but again that is personal opinion.
Summing up, I think Go is a language that both strives to be and is beginner friendly. Diverging from consistency in basic behaviors in the stdlib is not great, sharp tools not labeled as such is not great, etc, in my opinion. I know we don't agree and that's perfectly fine. I appreciate your reply. But I'm not trying to be disingenuous.
Just wanted to say - I've learned a tremendous amount from your blog over the years... I really enjoy reading it and hope you continue for a long time!
22 comments
[ 3.1 ms ] story [ 62.6 ms ] threadhttps://eli.thegreenplace.net/2019/on-concurrency-in-go-http...
Erlang, Rust and Go can only be used to solve embarrassingly parallel problems.
What language constructs/patterns do you feel are better suited to solving parallelism problems, and what languages exemplify these techniques?
Your article is from 2005, so I take it to mean that Java 6 was doing the interesting things you talk about - from my recollection, the go-to at that time was java.util.concurrent. (I haven't kept up with Java, so it could still be state-of-the-art.)
https://www.sciencedirect.com/science/article/pii/S016764230... - this paper describes the implementation as "The heart of the framework is maintenance of queues of blocked threads, which are restricted here to FIFO queues." If you're not familiar, this is exactly what go channels are, just baked into the language rather than in the standard library.
Additionally one benefit over Go, is that in Java (likewise in .NET), the scheduler algorithm is replaceable.
The more you downvote me the stronger I become! :D
I'm pretty sure each of those have been used for other kinds of parallelism than just embarrassingly parallel problems.
But use it if you really need it. Likely that’s not your bottleneck.
It’s a long read but it’s one of the most enjoyable things i’ve read in ages.
A beginner needs to be very careful and read the docs to catch e.g. this line:
"If ServeHTTP panics, the server (the caller of ServeHTTP) assumes that the effect of the panic was isolated to the active request. It recovers the panic, logs a stack trace to the server error log, and either closes the network connection or sends an HTTP/2 RST_STREAM, depending on the HTTP protocol. To abort a handler so the client sees an interrupted response but the server doesn't log an error, panic with the value ErrAbortHandler. "
I think that is an unintuitive assumption that makes for brittle software. Also, the fact that Go kind of blesses the use of panics internally in a library (e.g. "The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values.", https://blog.golang.org/defer-panic-and-recover) makes for even more brittle software, as predicting what state a control flow interruption like panic through a call stack leaves your data in can be challenging and require great care to avoid invariants being violated. Last I checked, e.g. Rust did not allow the reuse of data owned by a panicking call stack without explicitly asserting that it is still considered valid.
I guess I'm showing my bias for languages that make it harder to make mistakes, but I don't like brittle stuff like encouraging panic-like control flow for non-exceptional situations, implicit reuse of "panicked data", missing non-exhaustive match on enums, missing enums altogether, missing sum types and pattern matching making everything from detailed error management to proper state representation more brittle, etc etc. I'll stop there and not get into the rest of the stuff.
Go is one of the languages my employer pays me to write and I will say that I have a significantly higher appreciation for it now than when I started (it IS very beginner friendly and very ergonomic in general), but I wish it would help me more to write really robust and correct software.
In Go (as the blog post that I linked alludes to): "The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values."
However, it is not the expectation that libraries should recover from panics they didn't start themselves. If they did, it would be very hard to panic and actually have a natural expectation that your application would exit due to that panic, which is how most Go code behaves and expects to behave.
As I wrote in another reply, neither the main goroutine/function nor spawned goroutines automatically recover from panics. They DO shut down the entire server if any code in them panics (provided that no boiler plate recovery is performed at the root of the call stack, which in itself would make it very, very hard to reason about the consistency of the data that might have been touched before panicking at any one of countless of operations in the code in the call stack).
Therefore, one might also argue: Should the entire server shutdown if any worker thread causes a panic? I do agree that it is more plausible for an HTTP request thread to do so, but not enough to change such a basic behavior. Go doesn't allow to register a global panic handler to be able to perhaps recover but also log panics in a consistent way, such that it would be applied consistently across your entire process and customizable to the preferences of the developer as to their chosen trade off between "fail fast/never continuing processing in the face of unexpected programming errors" and "an unexpected programming error occurred but I still want to continue executing and hope that nothing broke in my application".
And I do acknowledge that different developers/organizations would want to make that trade off differently, but at present it is not very convenient to do consistently. The Go creators chose not to allow global panic handles (there are a bunch of discussions about it on Google Groups and similar, and I do agree with some of the arguments in them).
Some people (myself included) might prefer that the application fails and whatever orchestration manages this application triggers an alarm with operations staff, developers, etc, without instead risking that an application keeps running and perhaps due to some invariant now being violated and data inconsistent keeps making mistakes, perhaps serious mistakes.
This of course depends a lot on what kind of application you're building and how important this is, how much uptime for partial (but potentially buggy) functionality weighs against never risking serious mistakes. I tend to think that the correctness of most software in the world is actually important these days, but I fully admit there's a scale. Go however is being used to build all kinds of software these days.
If one doesn't like that strategy, and wants to build software that recovers in other fashions then perhaps one should have a look at Erlang and its process supervisor trees, or other systems with other trade offs.
It is a genuinely hard question, I admit that. I just don't think Go's stdlib in this case chooses a position on that trade off that I like, that's all. It's all opinion, and we're all entitled to them. Thanks for asking, and forcing me to put thoughts into words!
> The fact that the stdlib HTTP utils in Go recover from panics in handlers and provide no way of changing this
It's comically easy to just wrap the root handler, catch panics yourself, and explode.
> You need to provide your own error handling middleware (of which several exist).
And you need to provide your own handlers anyways, which seems reasonable to me, unless you're against writing code?
> A beginner needs to be very careful and read the docs to catch e.g. this line:
Yes you need to read the docs to understand what a function does. This does not require being very careful where I'm from.
> Also, the fact that Go kind of blesses the use of panics internally in a library ... makes for even more brittle software, as predicting what state a control flow interruption like panic through a call stack leaves your data in can be challenging and require great care to avoid invariants being violated.
They make this pretty clear, that you shouldn't leak the panic, in which case it's just an implementation detail. If your library is brittle, that's on the implementer. You don't have to use panics for control flow. It's just something you can do. A tool. Sure, maybe it's sharp.
> I don't like brittle stuff like encouraging panic-like control flow for non-exceptional situations
I've never seen anything remotely encouraging use of panic for control flow. The blog post even states it's uncommon and unusual. I don't know how you interpreted that blog post, which reads to me as "here's how defer works", to being "you should use panic for control flow whenever possible."
> It's comically easy to just wrap the root handler, catch panics yourself, and explode. > And you need to provide your own handlers anyways, which seems reasonable to me, unless you're against writing code? > Yes you need to read the docs to understand what a function does. This does not require being very careful where I'm from.
Yes, it's "comically" easy to do so if you know that you need to do it. I do it. I didn't say it was hard to do. But Go prides itself on being beginner friendly, having consistent behaviors and not having many ways of doing the same thing. The main function doesn't automatically recover from panics. Spawned goroutines don't automatically recover from panics. I feel like it wouldn't be outrageous to consider this an expected behavior for someone writing Go code, as very few libraries to my knowledge recover panics they didn't start themselves. The behavior of the stdlib HTTP utils diverge from that expected behavior. I think there are tons of developers in every language who don't necessarily scour the docs but assume consistency with some behaviors that seem basic and logical. Again, opinion, if that wasn't clear.
> They make this pretty clear, that you shouldn't leak the panic, in which case it's just an implementation detail. If your library is brittle, that's on the implementer. You don't have to use panics for control flow. It's just something you can do. A tool. Sure, maybe it's sharp.
Yes, they make it clear how it should be used if it is used, and I didn't claim otherwise. I also didn't claim that leaking it was the problem, but instead that maintaining invariants in the data of the library using the mechanism can be a hard thing to do, and whenever something is hard to do right it can contribute to brittle software.
> I've never seen anything remotely encouraging use of panic for control flow. The blog post even states it's uncommon and unusual. I don't know how you interpreted that blog post, which reads to me as "here's how defer works", to being "you should use panic for control flow whenever possible."
Yes, I will meet you half way here. :) This particular blog post doesn't encourage it, and at the moment I can't other any of the other places I've seen it. The blog post gives an example of Go stdlib using it, and then describes how to use it in a library if it is used. My personal preference would be to offer more caution or perhaps even discouraging in that blog post, but again that is personal opinion.
Summing up, I think Go is a language that both strives to be and is beginner friendly. Diverging from consistency in basic behaviors in the stdlib is not great, sharp tools not labeled as such is not great, etc, in my opinion. I know we don't agree and that's perfectly fine. I appreciate your reply. But I'm not trying to be disingenuous.
Not sure why the (2017) tag though, this post is from yesterday!