-
-
Notifications
You must be signed in to change notification settings - Fork 668
[WIP] Allow implicit conversion to shared. #8782
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
Conversation
so that methods that are shared can be called using unshared objects. This is fine because any extra synchronisation done as a result of being shared will not cause race conditions, see https://forum.dlang.org/thread/mailman.4068.1538360998.29801.digitalmars-d@puremagic.com
|
Thanks for your pull request and interest in making D better, @thewilsonator! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.
Please see CONTRIBUTING.md for more information. If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment. Bugzilla referencesYour PR doesn't reference any Bugzilla issue. If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog. Testing this PR locallyIf you don't have a local development environment setup, you can use Digger to test this PR: dub fetch digger
dub run digger -- build "master + dmd#8782" |
|
I can see the argument, but at the same time, if this type was designed to be used as |
|
|
Literally any thread-safe thing I write doesn't work single-threaded... I have a pattern where a scheduler manages jobs that specify the data they interact with, and in what way they interact; mutable, thread-safe, read-only. |
|
@jmdavis The 2 cases you describe are not this case. you describe shared -> shared (intended to be shared, written as such), or shared -> mutable (not-thread-safe; manually acquire a lock and case shared away)... mutable -> shared is safe, and very useful! It's unlikely that such an object exists that it is "shared and only shared". First; almost all objects begin life unique and initialise before being distributed. Initialisation is probably not thread-safe, so init methods aren't |
|
Thinking about this further, I can point out exactly why implicitly converting to |
|
Right, but two things to note: Implicit conversion to I realised I have neglected to mention that this is supposed to be for methods (i.e. |
|
@jmdavis Right, you've identified the key gotcha... the reference should be scope.
I don't think that's true... while a non-shared reference exists, no set of shared instances should exist, otherwise the thread-local instance might do something not-threadsafe even while the others are read-only-ing. I think |
|
Actually it should be fine for parameters as long as they are scope, because the function call will be synchronous w.r.t the current thread and scope guarantees that while the reference may be mutated as a result of the call, the reference will not outlive the call and so all is good. |
Right. So I think it's the case that this probably hasn't been revisited since |
If those shared references never modify the data (which |
|
I agree with Manu about
|
That's a really big and un-provable 'if'. I don't think that's reasonable. I think it's a strong specification in D that a thing is either thread-local, or is is shared. I am happy with that design decision; it makes implementation of shared methods possible within reason, and also will improve the perf of shared methods, since they don't have to account for the possibility of a non-shared doing random mutations. What is upsetting is that a thread-local thing can't call a shared method that definitely works... and as we've determined here, the reason is that the shared method might be impure. So, |
|
Fair enough.
Lets try doing that then, unfortunately easier said then done.
Agreed. I'm pretty sure thats what scope is for, but the exact inner working of -dip1000 hurt my head too (Any feedback on dlang/dlang.org#2453 would be useful to try and improve that situation). |
Yeah. In principle, that should be what it's for, but in practice, I've found that I've had to slap In any case, the whole mess just about makes me want to throw up my hands in the air over |
|
Well, @TurkeyMan can be our guinea pig. ;) and hope that its |
|
If can make it work, I will pig. |
|
Oh it can be done, I've just got to find all the places in the function matching code where I need to test for the conversion. |
|
|
|
Your PRs would get approved of we knew what you were trying to accomplish with them. Please review dlang/dlang.org#2453 |
Maybe, I should find the time to look at them, but honestly, the only piece of dmd that I've spent much time looking at was the lexer - and that was before the conversion to D (though it probably still looks mostly the same still). So, I just don't generally feel very qualified to review dmd PRs as far as the actual code goes. I can comment on language stuff like here, but the focus of my time on D simply hasn't been on the compiler, and any comments I would make would almost certainly be superficial. So, I feel kind of bad whenever you make comments like that, but I'm not sure that there's much that I can really do that would actually be useful. :| I certainly can't approve anything. |
|
I, personally, don't think that this change is justified. The fact that types that need synchronization are not implicitly convertible to types that do is a cool thing and I can't think of any situation where this is actually a liability. Of course, shared methods cannot be called on mutable objects, but that's not a problem and in most situations it's a good thing because it makes it clear that a specific function adds the overhead of synchronization. I don't see a valid use case where existing methods fail to solve the problem and where this conversion is actually needed. |
I have a code-base that a billion dollar company is staking its future on that says otherwise. This problem space is modern and critically important. Also, Walter is all over this space; trying to gear D for the future is probably his hottest ambition (judging by his various lectures).
It's weird, because I don't see a single use case where this isn't needed. Everything that may be shared may also not be shared (or not shared yet, or was shared but isn't now)... It's unacceptable that the thread-safe API for the object is inaccessible because there's only one instance. Here's one to consider: struct Logger
{
void log(string line) shared scope; // <- logging API is thread-safe, naturally.
void flush(); // flush function not-threadsafe. called once per cycle during a sync-point
}
void work(ref shared Logger b)
{
b.log("Hello"); // i'm a worker that does logging
}
void finaliseCycle(ref Logger b) // <- I have the only one in existence
{
b.log("Finalising..."); // ERROR: wat?!
//...
b.flush(); // natural time to flush for this cycle
}A thread-safe API like this collects data into thread-local pools and then reconciles later... the idea that you can't log during the sync-phases is just... really annoying. This pattern propagates to literally everything I have that would make use of |
|
Honestly, I find it concerning if an API is designed to work as both thread-local and That being said, what ultimately matters here is correctness of the type system. As far as I can tell, implicitly converting a thread-local object to |
I surely that's the entire point of a
An object is thread-local whenever it isn't distributed... and that could be as often (or not) as the program likes. During those moments it's possible to access a wider range of not-threadsafe functionality, but that doesn't mean the threadsafe functionality should go away.
That's a tremendously shit way to interact with shared stuff... it's not really even 'shared'. Locking an external mutex asserts a state where it's not shared at all, and casting it away just commits to that situation. What you're describing is NOT interacting with a shared thing... you're just interacting with a thread-local thing. Using an outer mutex to force a context where the thing is thread-local for some period while you do thread-local stuff is not For my money, this has absolutely nothing to do with shared. A
You said 'mutex' and 'cast away' 4-5 times... you are evidently not writing super-scalar code.
It's certainly not 'pointless', because the function does the function that the function does, which is intended by the call ;) Let's say the shared method is called 100's or 1000's of times from shared code, and exactly once from thread-local (and not-hot) code... is it worth the busy work to write an 'efficient' non-shared overload for that one call? That's a DRY violation, and added maintenance burden... increasing tech-debt for what gain? The thread-safe version of the method can obviously handle a thread-local call. Anyway, there's all this talk of mutexes and casting. That's not 'shared' at all, that's just controlled thread-local access. The feature is completely worthless in those terms. If that's the design goal of shared, then I suspect it was designed by people that had no idea what they were trying to achieve... that's certainly not what the future of software looks like! |
|
Hmm, you could auto-implement the unshared methods to cast |
|
@thewilsonator Possible. But that's basically an admission of defeat that the design of shared is a failure. |
|
On some level, the compiler currently fails on the third one. It's improved over time, but a number of operations which are not thread-safe are still allowed (like assignment or copying). That's likely to be sorted out when Walter and Andrei finally sit down and finish the final details of In general,
Of course, other synchronization tools such as semaphores also exist, but in principle, they're just
If you don't want to use mutexes, and you'd prefer to do all of your multi-threaded stuff with atomics and semphores and the like, that's fine. There are obviously use cases where atomics are more appropriate than mutexes, though I would have expected mutexes to be by far the more common use case. Regardless, |
|
More discussion here: https://digitalmars.com/d/archives/digitalmars/D/shared_..._319797.html which pretty much renders this unworkable in its current form. Reopen if these problems are addressed. |
|
I suppose that it should be unsurprising that Timon noticed something that we missed. He tends to do that. |
|
Timon is right far too often :-) But hey, we sure need him. Having to swallow our pride is better than investing a lot in huge, embarrassing mistakes. I'm glad when he proves me wrong. |
|
...are we talking about the "scope is not transitive" comment? Is that the case? How is it that scope is not transitive? That seems un-workable? |
Unfortunately yes
It was always this way AFAIK, even if some people (including myself) argued for it to be. |
|
Can we continue riffing on this with a test for |
|
I won't have time to work on this before I see you, we can talk about it then. |
so that methods that are shared can be called using unshared objects. This is fine because any extra synchronisation done as a result of being shared will not cause race conditions, see https://forum.dlang.org/thread/mailman.4068.1538360998.29801.digitalmars-d@puremagic.com
This probably doesn't catch all edge cases of this change, hence WIP. Also test cases etc.