for _, lock := range locks {
lock.Lock()
defer lock.Unlock()
}
To acquire locks in a loop and release them at the end of a function. Otherwise the previous lock would be dropped each time the loop executes.I've never seen the advantage to having a block structured defer: it's easy to add a new function, it's not always possible to remove a block.
Go can allocate defers on the heap, but that's a different story.
The pthread_cleanup_push/pthread_cleanup_pop thing presumably keeps its own heap-allocated vector, backed by malloc or something. C itself can't willy nilly heap-allocate, so that list will be on the stack. But the stack is tiny compared to how long loops can be. Hence stack overflow.
Given a possible implementation, this loopy mutex example could have a tiny stack footprint: a single pointer to the shared cleanup() function; and a single pointer to the head of a (heap allocated) linked list of pointers to mutex (i.e., the function arguments). And the function pointer would not necessarily require allocation at all, as we can statically point at the function definition here. So we are down to a single word of stack allocation.
In the general case I see no alternative to either the compiler generating one alloca() per defer or heap allocating defer callbacks. Both are terrible solutions for C, because alloca can overflow, while heap allocations can fail with an error code and defer has no way to catch that error. Besides, C programmers just won't use the feature if it requires allocation out of performance concerns. Block-scoped defer is the only reasonable semantics.
> C programmers just won't use the feature if it requires allocation out of performance concerns.
I agree that many C programmers wouldn't touch the feature for performance reasons. But let's not pretend that every C program is a video driver, a AAA game or a web engine. Many, many large C programs would benefit immensely from `defer` semantics -- otherwise, why would the GCC feature exist -- and they are performance-tolerant enough that a little heap allocation would be a reasonable tradeoff for increased safety.
But I'm not really defending `defer` in the first place...
> Block-scoped defer is the only reasonable semantics.
I agree with you completely. :) I was never defending function-scoped `defer`, but answering claims about the necessity of stack allocation. There are possible defer implementations that wouldn't blow the stack: that's my only point.
While true, the culture in C and C++ circles pretends otherwise, hence why we have unsafe by default and safety as opt-in in those languages.
The sky would fall if we lose that 1us.
It's healthy for culture-conscious programmers to reflect, now and then, on just how small a segment they represent. Cultured programming is fine, but it's like opera: the ordinary programmer recognizes a few of the tunes, but they don't sing along. It's hard to appreciate just how much uncivilized business code is out there, when nobody is getting HN likes for keeping that 1980's ERP running.
Nevermind embedded platforms where heap might be unavailable or just so scarce that its use beyond early initialization is strictly verboten. Or interrupt handlers where you simply can't call an allocator.. Block scoped defer could still be useful on such systems (e.g. with locks).
...But as I pointed out in a sibling comment, I was never defending function-scoped defer. I agree with you. I was pointing out that the implementation of such a feature wouldn't require excessive stack allocation.
But I looked through the spec again, and it actually just says that defer outside the top level is implementation-defined, so this is irrelevant anyway.
This has been a fun conversation, and I really enjoyed chatting with you about this. :) Take care.
for _, lock := range locks {
lock.Lock()
// Do something with lock
}
And I don't really do Go, but in Rust, if I wanted to lock some arbitrary list of locks for a whole function, it would just be something like this at the top: let _ = locks.iter().map(|l| l.lock().unwrap()).collect::<Vec<_>>();that looks like a super big gotcha, hasn't there been enough bugs with alloca() being called in a loop yet to show how risky and unintuitive this is ? block-scoped is explicit and explicit is good
If I had to choose between allowing that pattern and allowing reasonable usage in all control constructs, I'd choose the latter.
Also, Wikipedia (https://en.wikipedia.org/wiki/Deadlock, https://en.wikipedia.org/wiki/Deadlock_prevention_algorithms) doesn’t seem to know about it. Even though I expect/guess it to be popular in embedded work, that makes me wonder whether lock ordering is used much.
Could also be an omission in Wikipedia. It has https://en.wikipedia.org/wiki/Banker%27s_algorithm, which I hadn’t heard of, and that Wikipedia says of
“In most systems, this information is unavailable, making it impossible to implement the Banker's algorithm. Also, it is unrealistic to assume that the number of processes is static since in most systems the number of processes varies dynamically”
Function-scope defer is something that surprises everyone I explain it to. Programmers naturally expect defer to be block-scoped.
As far as I know, most functions have only one defer, and Go optimizes such cases quite trivially, by inlining calls to the deferred function at every function exit at compile time, without managing an additional stack. If there are several defers, then yes, the slow path is used.
Is it worse than in C that there is no concept at all? Is it better that everyone is on their own to do it their own way every time?
I’m not above making the eaiser, but to your point when I see their example of:
double* q = malloc(something);
defer [qp = &q]{ free(*qp); };
That doesn’t look like the C that I know.What's the alternative, though? Sprinkle your code with try..finally's? RAII like in C++?
Also why is RAII bad? It's an awesome feature in C++.
I didn't mean it's bad (I used to be a C++ developer myself and enjoyed RAII a lot), just wondering what are the alternatives that the OP doesn't consider "hacks". RAII would require to introduce constructors/destructors in the language, with all the gotchas (and probably you'll want a full-fledged OOP after that), which is apparently against Go's design principles as a simple language.
>Auto-Closable interface with syntactic support.
I don't see much difference here in practice; the whole difference is that in C#, for example, you use "using" on a whole object, while in Go it's a "defer" on a specific method of the object (or a standalone function). You are not limited to a single method and can use it on any method you deem necessary.
Auto-closeable/RAII, however, is less flexible in ad hoc situations specific to a certain function (you have to define dummy classes just to make sure a function is called no matter what), Go allows to use "defer" on a lambda. Auto-closeable also ties control flow to an object, which makes sense in an OOP-focused language, but Go isn't one.
Otherwise a generic _bracket_ operator could be introduced, similar to Python's `with` statement. In C it might be a bit hard to parameterize, but even if it just requires you to use a void, it's still an improvement vs. not having anything at all. (Talking about language features so `__attribute__((__cleanup__))` doesn't count.)
Imagine something like this:
FILE* handle = bracket(fopen("data.bin", "rb"), fclose) {
fread(..., handle);
return;
fwrite(..., handle);
} // <-- implicitly calling fclose(handle), always
Edit: After writing this sample I could even an optional `else` block in case `fopen` returned `NULL`.Edit2: Of course, syntax fine-tuning would be welcome. For instance, `handle` should be restricted to the bracket's scope.
Zig has errdefer which only runs if an error occurred below the statement within that scope. It allows you to always keep cleanup locally, but you can still handle errors for your business logic somewhere else.
errdefer is cool, but I don't think C should support it. Mainly because you'd need to spec errors at a language level, and doing that today is probably impossible.