-
-
Notifications
You must be signed in to change notification settings - Fork 741
add initOnce #2664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add initOnce #2664
Conversation
MartinNowak
commented
Nov 7, 2014
- uses double-checked locking (the correct one)
- all instances shared the same global mutex
- minimal overhead and simple API
There is a potential for dead-locking here when one thread waits in |
* Executes the given $(D_PARAM callable) exactly once, even when invoked by | ||
* multiple threads simultaneously. The implementation guarantees that all | ||
* calling threads block until $(D_PARAM callable) returns and that any | ||
* side-effects of $(D_PARAM callable) is globally visible afterwards. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: is -> are
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
61d7958
to
53412fc
Compare
Did you measure whether the variant using atomics or the TLS one are actually better for typical applications (typical of course being hard to define)? I'd actually expect the latter to be faster in cases where there are a lot of concurrent accesses. Furthermore, I don't think the dead-lock issue should be brushed aside so lightly. Concurrency primitives that come with hidden traps are rarely a good thing. |
No, there is going to be only a single write (no contention), acquire/release is good enough (and cheap or even free) and TLS access is slower (emulated TLS). |
I added a note in the docs. But I could still only come up with very contrived examples where this would be a problem. |
Usual ping to @complexmath |
Depending on the platform/TLS model TLS accesses might be nearly free too, but I guess you are right – in general, they aren't, and at least on x86 the read should be free (unless you get extremely unlucky with what else is on your cache line, of course, but that's beside the point here). |
996d8bc
to
2e80999
Compare
Are we still waiting for something? |
07c94d0
to
2e80999
Compare
I'd like one or two more opinions before adding a new API artifact, just as a matter of principle. |
{ | ||
import core.atomic; | ||
|
||
static shared bool flag; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe silly question, but should not be used __gshared here instead of shared?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used in atomic operations, therefor requires shared. And shared static variables are global, not thread-local, just like __gshared.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MartinNowak thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also I think general rule of a thumb is to always try going for shared
instead of __gshared
unless there are some transitivity issues that prevent it (like with Mutex
)
I'd like to help review this, but my experience with concurrent programming is rather limited, so I don't know if I can give a meaningful review. But at least on the surface, the code looks OK to me. |
Hm, one additional, non-obvious pitfall is that multiple This way, the deadlock issue would by default drastically be reduced in scope to areas that are already coupled in source code, as we'd just put the Mutex in that object. If the few extra bytes would be critical for a given application, the user could always make the decision to re-use it on their own. You could also make specifying the |
Agreed. |
* $(D_PARAM callable) should not wait for another thread that might | ||
* also invoke callOnce or a dead-lock can happen. | ||
*/ | ||
void callOnce(alias callable)() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to have a template constraint making sure that callable
is callable?
@MartinNowak this implenentation looks correct, but I think that global mutex with static this (which may be instantiated more then one time) is stodgy. Why don't you use bool flag instead of the second mutex?
|
Yes, and that's perfectly fine. Because it means that multiple invocation to say a library initialization will only happens once, it's a sane default. You can easily workaround it by wrapping your function call in a lambda.
No, I thought about this quite a while and I don't think it's worth the trouble.
A |
It's also perfectly unexpected if you think of it as "do this function call once". Your "easy" workaround will appear completely mysterious to many users, because it relies on the fact that two semantically identical lambdas are still distinct symbols – something that, if I remember correctly, by the way isn't even documented right now. Note that you can write your library initialization very naturally in the other model too, and this time without relying on non-obvious details: void apiClient1() { ensureInitialized(); … }
void apiClient2() { ensureInitialized(); … }
void ensureInitialized() { callOnce({ auto h = dlopen(…); … }); }
"much more complex"? It adds a single type. I don't think it would necessarily entail more documentation (and certainly not "way"), if you, from a user's point of view, properly documented the details of your proposal.
The problem that exists in your implementation certainly isn't; it is at least as big as with a separate once_flag, and in my opinion much worse (as you can't even see the sharing in the code). There might be a different, but let's keep the discussion to the point.
That is indeed an issue to consider.
I don't think your API is conceptually more simple than having a separate flag. The latter can easily be explained as the thread-global equivalent of
Err, yes, of course. |
To cite http://en.cppreference.com/w/cpp/thread/call_once Exactly one execution of exactly one of the functions (passed as f to the invocations in the group) is performed. It is undefined which function will be selected for execution. The selected function runs in the same thread as the call_once invocation it was passed to.
It's mentioned in the documentation, and because it's a recursive mutex you'd need to interact with another thread that already invokes callOnce. I don't agree that this is a very practical problem, but we could add an overload to callOnce that takes a specific mutex argument to resolve it.
Sure it's cleaner if the library provided a thread-safe initialization function, but if it doesn't you're stuck. And if you have multiple libraries in your app that use the same library you'll end up initializing it twice.
It saves the not so trivial boilerplate of declaring and initializing a separate flag. class Foo
{
private static shared OnceFlag fooFlag;
shared static this()
{
fooFlag = OnceFlag();
}
Foo instance()
{
callOnce!({ _instance = new Foo(); })(fooFlag);
return _instance;
}
}
I added an example for this, still missing a valid use-case where you actually want this behavior. |
It's simpler because there is no lock order issue. The following code will always dead-lock, but again I can only come up with very contrived examples. callOnce!({
auto tid = spawn({
callOnce!({writeln("foobar");})();
ownerTid.send(true);
});
receiveOnly!bool();
}); |
Does it happens because you use one global mutex for all callOnce instance? Why don't use separate mutexes/flags for each callOnce instance? |
It would be much harder to dead-lock with a mutex per instance.
Because it's a waste of resources, the mutex would at maximum be used once. |
Whether to use lazy or eager initialization (and whether to use compile-time or runtime initialization in the eager case) is orthogonal from whether it's TLS or global. TLS objects benefit from lazy initialization in exactly the same ways:
It's cluttered imperative boilerplate: static bool isInitialized = false;
static T theSingleton;
if(!isInitialized)
{
theSingleton = ...;
isInitialized = true;
}
// use `theSingleton` vs declarative: static T theSingleton;
initOnce!theSingleton(...);
// use `theSingleton` Of course the global singleton is much more involved and error-prone than the TLS one, but edit: To be clear, I'm not against putting it in std.concurrency, I think that's perfectly reasonable. |
@JakobOvrum The point is that
Most likely has to be:
So for singletons that's the same accessor boilerplate anyway and trading one for another really is not a deal breaker. |
Right sorry for confusion. |
Firstly, it's clearly not the same amount of boilerplate. But more importantly, lazy initialization is not just for the singleton pattern. |
You correctly identified the usability problem. Now it happens quite often to me that I accidentally declare a thread-local variable instead of a global one. That would result in quite some debugging and confusion if |
But almost and we have the policy to not add trivial stuff to phobos. static Instance var;
if (!var) var = new Instance(); |
I think this is a good point for
Most lazily initialized variables will be of reference type, so I guess that's a good point. |
Updated and all things addressed, at least I hope so ;). |
Thanks @JakobOvrum, this really came out much better than the initial |
Partly fixed now, I attached each unittest example to it's own overload, so the one taking a mutex is separated a little better. |
auto ref initOnce(alias var)(lazy typeof(var) init, Mutex mutex) | ||
{ | ||
// check that var is global, can't take address of a TLS variable | ||
static assert(is(typeof({__gshared p = &var;})), "var must be 'static shared' or '__gshared'."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are TLS variables disabled when the type system prevents you from doing it wrong anyway?
Why are you starting that discussion again? This is a primitive for race-free lazy initialization. There are no races when assigning TLS variables.
Most of the time it's not even possible to use static Mutext mutex; // should be __gshared
void foo()
{
synchronized (initOnce!mutex(new Mutex)) {
// Oh, not mutual exclusive at all. Who's going to debug this?
}
} |
Anything left to do? |
ping |
* simultaneously calling initOnce with the same $(D_PARAM var) argument block | ||
* until $(D_PARAM var) is fully initialized. All side-effects of $(D_PARAM init) | ||
* are globally visible afterwards. | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing Params:
block
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I seriously write var - the variable to initialize
and init - the initializer
even though the first sentence perfectly describes what this function does? This feels a lot like auto sum = a + b; // add a and b
comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. The main description can be more of an overview, and be more of an explanation of what the point of this function is. For example:
http://www.cplusplus.com/reference/cmath/sin/
That's normal and expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, but I still disagree.
We don't improve our documentation by adding noise.
What's that supposed to tell me?
http://www.cplusplus.com/reference/cmath/pow/#parameters
|
||
/** | ||
* Initializes $(D_PARAM var) with the lazy $(D_PARAM init) value in a | ||
* thread-safe manner. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please add a blank line to make the first part a summary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
- uses double-checked locking (the correct one) - minimal overhead and simple API - default mutex is shared among all instances
Anything left to do? This has been on hold for yet another week and almost 2 month in total. |
Ping again |
LGTM |
Someone merge this please? |
Auto-merge toggled on |