Skip to content
This repository has been archived by the owner on Oct 12, 2022. It is now read-only.

fix issue 16230 - core.atomic.atomicLoad removes shared from aggregat… #1605

Merged
merged 13 commits into from Sep 1, 2017

Conversation

aG0aep6G
Copy link
Contributor

@aG0aep6G aG0aep6G commented Jul 3, 2016

…e types too eagerly

https://issues.dlang.org/show_bug.cgi?id=16230

This breaks two places in phobos. Pull request follows.

@dlang-bot
Copy link
Contributor

dlang-bot commented Jul 3, 2016

Thanks for your pull request, @aG0aep6G! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.

Some tips to help speed things up:

  • smaller, focused PRs are easier to review than big ones

  • try not to mix up refactoring or style changes with bug fixes or feature enhancements

  • provide helpful commit messages explaining the rationale behind each change

Bear in mind that large or tricky changes may require multiple rounds of review and revision.

Please see CONTRIBUTING.md for more information.

Bugzilla references

Auto-close Bugzilla Description
16230 core.atomic.atomicLoad removes shared from aggregate types too eagerly

@aG0aep6G
Copy link
Contributor Author

aG0aep6G commented Jul 3, 2016

This breaks two places in phobos. Pull request follows.

dlang/phobos#4551

The phobos changes need to be pulled first. But before that, the changes here should be reviewed.

@PetarKirov
Copy link
Member

LGTM

@MartinNowak
Copy link
Member

Looks goods, could we do it w/o the breakage, e.g. by instantiating a deprecated HeadUnsharedDeprecated for the old conversion?
IIRC there was a bug that kept dmd from emitting deprecation warnings for template instantiations.

else
alias T HeadUnshared;
alias HeadUnshared = shared T;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HeadUnshared didn't remove shared from structs before your PR, so the bug description is confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HeadUnshared didn't remove shared from structs before your PR, so the bug description is confusing.

The problem is that HeadUnshared is never instantiated with fully shared types, only ever with tail-shared ones or completely unshared ones. That happens because atomicLoad itself strips shared already, and only then does it apply HeadUnshared. That means, the instantiations of HeadUnshared have been nops.

Now, shared needs to be re-added at some point to fix the issue with structs. I hadn't really thought this all through before, so my solution was a bit hacky.

I've now renamed HeadUnshared to TailShared and made it so that it constructs a type with a shared tail, ignoring T's given shared-ness. If T has no tail (no indirections), an unshared T is returned. If T's tail cannot be typed independently (aggregates with indirections), a shared T is returned.

Hope it makes more sense now. Tell me if this version is better. I'd squash the commits then.

@aG0aep6G
Copy link
Contributor Author

aG0aep6G commented Jul 8, 2016

Looks goods, could we do it w/o the breakage, e.g. by instantiating a deprecated HeadUnsharedDeprecated for the old conversion?

I don't know how a deprecated atomicLoad could be distinguished from the fixed one. The parameters aren't changing, only the return type does.

@MartinNowak
Copy link
Member

I don't know how a deprecated atomicLoad could be distinguished from the fixed one. The parameters aren't changing, only the return type does.

Well, I though we can attach a deprecation message to template instantiations of TailShared that will change their behavior.
Your change has a simple consequence, it returns shared class references instead of plain class references. So you can even catch this with a deprecated atomicLoad specialization.
In any case we can't merge this with the breakage.

@@ -76,10 +126,10 @@ version( CoreDdoc )
* Returns:
* The result of the operation.
*/
HeadUnshared!(T) atomicOp(string op, T, V1)( ref shared T val, V1 mod ) pure nothrow @nogc @safe
TailShared!T atomicOp(string op, T, V1)( ref shared T val, V1 mod ) pure nothrow @nogc @safe
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't do ops on classes, so this is more permissive than before.
Can you recheck that TailShared!T is always implicitly convertible to shared T?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't do ops on classes, so this is more permissive than before.

Sorry, I don't follow. What am I allowing that wasn't allowed before?

Can you recheck that TailShared!T is always implicitly convertible to shared T?

I can put static assert(is(TailShared : shared T)); at the end of TailShared. Is that what you have in mind?

By the way, are the implicit conversion rules on shared sound? D lets me copy large types (that can't be loaded/stored atomically) to and from shared. Shouldn't that be disallowed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't do ops on classes, so this is more permissive than before.

Sorry, I don't follow. What am I allowing that wasn't allowed before?

This function atomicOp can't be used w/ classes, TailShared!T is less restrictive than HeadUnshared!T (IIRC), so that function doesn't need any deprecation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function atomicOp can't be used w/ classes, TailShared!T is less restrictive than HeadUnshared!T (IIRC), so that function doesn't need any deprecation.

Ok, so atomicOp cannot possibly work correctly with classes, right?

The current code, using HeadUnshared, fails on the cas call. Looks like a lucky coincidence that the atomicOp call is rejected.

With TailShared it goes through if and only if the class has both a shared and an unshared opOpCall, because the template constraint checks the unshared case, and the actual operation is done on the result of TailShared, which is shared for classes.

The same thing happens with structs that have both variants of opOpAssign.

So, it's not just classes. atomicOp can't work with types that have an inseparable head and tail. Can't call an unshared opOpAssign, because it would assume that the referenced data is unshared. Shouldn't call a shared opOpAssign, because atomicOp would just add overhead then, and the shared opOpAssign may be implemented with locks instead of atomic operations.

So, add a constraint !is(TailShared!T == shared)? Does that make sense?

@aG0aep6G
Copy link
Contributor Author

aG0aep6G commented Jul 9, 2016

Well, I though we can attach a deprecation message to template instantiations of TailShared that will change their behavior.

A deprecation means that the thing should not be used, and that it's going to get removed. But the changed parts should be good to use, and their removal is not planned.

If a deprecation stage is a must, then I think we have to add a new function that replaces atomicLoad (and probably the same for atomicOp). Then atomicLoad and atomicOp can be meaningfully deprecated.

@MartinNowak
Copy link
Member

A deprecation means that the thing should not be used, and that it's going to get removed. But the changed parts should be good to use, and their removal is not planned.

Yes, but it also means that we're going the break code, so deprecated("Cast away shared if necessary") HeadUnshared!T atomicLoad() if (is(T == class))` does make sense.

@aG0aep6G
Copy link
Contributor Author

aG0aep6G commented Aug 8, 2016

Yes, but it also means that we're going the break code, so deprecated("Cast away shared if necessary") HeadUnshared!T atomicLoad() if (is(T == class))` does make sense.

But then I have no way to call the fixed version. So I can't fix my code properly until the old version is actually removed and the new one takes over. And until then I have to live with the deprecation message.

To me that would be more frustrating than just getting an error.

@dlang-bot dlang-bot added the Bug Fix Include reference to corresponding bugzilla issue label Jul 13, 2017
@aG0aep6G
Copy link
Contributor Author

@MartinNowak (or anyone else), it's been about a year. Can we find a way forward for this? This is a safety issue that should be resolved.

I'd love to mark the old behavior as deprecated, but I don't see how.

The only way I see is to add the new behavior under a new name, and deprecate the old name. Eventually, when the old stuff has been removed, the new stuff can get the old name. If I should go for this, please say so.

Otherwise, I think I need it spelled out in detail how you'd do this.

@aG0aep6G
Copy link
Contributor Author

@MartinNowak (or anyone else), it's been about a year.

Another week later. Anyone?

@PetarKirov
Copy link
Member

@aG0aep6G I'll take a look shortly.

alias S = shared T;

static if (is(S U == shared U)) {}
else static assert(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs an error message

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs an error message

Added a message: "S should be shared." But I'm not sure if it's any good. The assert shouldn't ever be hit, no matter what T is. So I'm not sure what expectation to express in the message.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would specify that it should never be hit in the error message and the reason why (the latter of it looks like you've already done).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would specify that it should never be hit in the error message and the reason why

I've added some more text. I may have gone overboard. If it's too verbose now, I'd appreciate a concrete suggestion.

@MetaLang
Copy link
Member

@ZombineDev not sure if you've seen it but this has a corresponding Phobos pull that should be reviewed and pulled as well this one: dlang/phobos#4551

@PetarKirov
Copy link
Member

PetarKirov commented Jul 29, 2017

@aG0aep6G Sorry for taking so long to get back to you, I had limited time.

This PR looks good to me, essentially making this module work as I imagined it would before looking at the code. I have only one request - can you please add a changelog entry detailing the before/now difference (HeadUnshared vs TailShared behavior), as this is an important change IMO.

@aG0aep6G
Copy link
Contributor Author

can you please add a changelog entry

done

Copy link
Member

@PetarKirov PetarKirov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @aG0aep6G, looks great! The only thing left is to resubmit the Phobos PR.

@aG0aep6G
Copy link
Contributor Author

Cool. @MartinNowak ?

ping

@PetarKirov PetarKirov assigned jmdavis and unassigned MartinNowak Aug 14, 2017
@PetarKirov
Copy link
Member

It seems Martin is too busy at the moment. Jonathan, could you take a look?

Copy link
Member

@MartinNowak MartinNowak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, this still breaks a prominent use-case of core.atomic.

import core.atomic;

shared Object so;

void test()
{
    Object o = atomicLoad(so);
}

Second, I'm somewhat warily of the recent trend to enforce more usage of shared. While it's good to be correct, shared still isn't a full mature language feature.
Now this PR changes the basic primitive to interact with shared data from one brokenness to a different one, i.e. from overly loose (but possibly correct depending on the programmer) to overly restrictive (but requiring the programmer to add correct casts).
Ultimately I'm worried a bit this will drive users to a cast() attitude, making shared basically useless.

How about we try a different correct approach, implement a HeadUnshared that actually wraps the return type (similar to Rebindable), and has @trusted @property ref getters, that returns the appropriate field types. Mostly structs returned from atomicLoad are just plain data fields.
Implicit conversion via alias this would still return a shared struct/class, to avoid any holes by calling unshared methods.

@MartinNowak
Copy link
Member

Wrapping a struct is somewhat tricky for private fields.

@PetarKirov
Copy link
Member

First of all, this still breaks a prominent use-case of core.atomic. [...]

I don't think we need to discuss why this code is broken. shared is not mature language feature, so I don't think we can guarantee it's stability in the short term (1-2 years), although I strongly agree that we shouldn't go cowboy style and trigger-happy follow the "break my code" advice from users. But in this case, I would rather help users see their potential mistakes rather than people seeing D as a language with no focus on safety and with ugly legacy baggage (functions that don't quite work correct but weren't fixed). Today most of druntime/phobos doesn't work correctly with shared. If this PR is merged, we will have one less wrong thing to worry about, and one that is quite important.

Second, I'm somewhat warily of the recent trend to enforce more usage of shared. While it's good to be correct, shared still isn't a full mature language feature.

I see this the other way around. For someone coming off from TDPL, shared promises a bright future, but in reality it under-delivers. People are not trying to use shared "just because". Instead we are trying to make shared more mature and useful. We can't present shared to people unless at least we have it working correctly with the standard library. core.atomic plays a fundamental role in building the library stack.

Now this PR changes the basic primitive to interact with shared data from one brokenness to a different one, i.e. from overly loose (but possibly correct depending on the programmer) to overly restrictive (but requiring the programmer to add correct casts).

I see your point, but I don't agree with you. It's like saying:
"This proposal to the language C - disallowing implicit casts from void* to T* changes the basic primitive to interact with pointers from one brokenness to a different one, i.e. from overly loose (but possibly correct depending on the programmer) to overly restrictive (but requiring the programmer to add correct casts).

Ultimately I'm worried a bit this will drive users to a cast() attitude, making shared basically useless.

Looking at the project tester it looks like this is already the case :( . People either don't use shared correctly, put casts everywhere, or ignore it completely by using __gshared. The only way forward is to make using shared practical (by making adding needed functionality to the library) and useful (by catching potential mistakes).


How about we try a different correct approach, implement a HeadUnshared that actually wraps the return type (similar to Rebindable), and has @trusted @Property ref getters, that returns the appropriate field types. Mostly structs returned from atomicLoad are just plain data fields.
Implicit conversion via alias this would still return a shared struct/class, to avoid any holes by calling unshared methods.

I also thought about writing a proper typecons for shared (in my case it was Synchronized(T, Mutex = core.sync.mutex.Mutex) meant to emulate the peeling of shared that synchronized blocks are meant to do), and in general I like the idea, but I don't know if it's doable in practice for atomicLoad.

  • May break generic code that introspects the return type in unforeseeable ways
  • Aggregate scoped imports can't be introspected (AFAIK), so we can't emulate them
  • Not sure if it could be made to work with classes.
  • What would these properties return? In the example struct S { int head; int* tailPointer; }, what would the ref tailPointer() @property return if you call atomicLoad!(S*) - shared(int*) or shared(int)*? If it would be shared(int)*, then this property would need to implicitly call atomicLoad, but then how would you atomically set it? If the answer is shared(int*), then there would be no point in having a wrapper. And returning int* is just wrong.

I'm not saying that I'm in principle against this idea. I just think that this need to be very carefully designed, it is out of scope for this PR and could easily be a breaking change to atomicLoad's return type. Probably sounds more like a job for something called ImplicitlyAtomic(T).

@MartinNowak
Copy link
Member

MartinNowak commented Aug 17, 2017

What would these properties return? In the example struct S { int head; int* tailPointer; }, what would the ref tailPointer() @Property return if you call atomicLoad!(S*) - shared(int*) or shared(int)? If it would be shared(int), then this property would need to implicitly call atomicLoad, but then how would you atomically set it? If the answer is shared(int*), then there would be no point in having a wrapper. And returning int* is just wrong.

struct S { int head; int* tailPointer; }
atomicLoad!(shared(S*)) -> shared(S)*
atomicLoad!(shared(S)) -> HeadUnshared!S

struct HeadUnshared!S
{
    @property ref int head() { return impl.head; }
    @property ref shared(int*) tailPointer() { return impl.tailPointer; } // just use atomic* on the second layer yourself
    alias this __impl;
    shared S __impl;
}

Sketch

struct HeadUnshared(T)
{
    static foreach (mem; __traits(getMembers, T))
    {
        alias MemT = typeof(__traits(getMember, T.init, mem));
        static if (is(MemT == function))
            continue;
        static if (hasIndirections!MemT)
            mixin("@property ref shared(MemT) " ~ mem ~ "() @safe { return __impl. " ~ mem ~ "; }");
        else
            mixin("@property ref MemT " ~ mem ~ "() @trusted { return *cast(MemT*)&__impl. " ~ mem ~ "; }");
    }
    alias this __impl;
    shared T __impl;
}

I'm not saying that I'm in principle against this idea. I just think that this need to be very carefully designed, it is out of scope for this PR and could easily be a breaking change to atomicLoad's return type. Probably sounds more like a job for something called ImplicitlyAtomic(T).

Sure, but this PR is already a breaking change, so it's a good time to look at where we want to go.

@aG0aep6G
Copy link
Contributor Author

As far as I see, the wrapper idea can only work as intended with structs. With a class, atomicLoad only loads the class reference. All fields have to be treated as shared. Doesn't matter if they have indirections or not. I don't see Wrapper!C being an improvement over shared(C) then. All it achieves is removing the shared qualifier from the top level, at the cost of being a completely different type.

For structs, it has more appeal. Instead of generating properties, we could also generate a struct type that has the same fields, but applies TailShared to their types:

template TailShared2000(T)
{
    static if (is(T == struct) && is(TailShared!T == shared))
    {
        struct TailShared2000
        {
            private import std.traits : hasIndirections, isAggregateType;
            
            static foreach (i; 0 .. T.tupleof.length)
            {
                mixin("TailShared!(typeof(T.tupleof[i])) " ~
                    __traits(identifier, T.tupleof[i]) ~ ";");
                //TODO: alignment
            }
            
            @property shared(T) _fullyShared()
            {
                return * cast(shared(T)*) &this;
            }
            alias _fullyShared this;
        }
        static assert(TailShared2000.sizeof == T.sizeof);
    }
    else alias TailShared2000 = TailShared!T;
}
unittest
{
    struct S { int head; int* tailPointer; }
    TailShared2000!S s;
    static assert(!is(typeof(s) == shared));
    static assert(is(typeof(s.head) == int));
    static assert(is(typeof(s.tailPointer) == shared(int)*));
    static assert(!is(typeof(s) : S));
    static assert(is(typeof(s) : shared S));
}

@PetarKirov
Copy link
Member

PetarKirov commented Aug 17, 2017

Keep in mind that atomic loading a whole struct is rather rare, since at most you can load 2 * size_t.sizeof.
On most architectures other than x86, you can load only a single size_t.
(for easy ref: https://github.com/tc39/ecmascript_sharedmem/blob/master/MACHINES.md)

@MartinNowak
Copy link
Member

MartinNowak commented Aug 18, 2017

As far as I see, the wrapper idea can only work as intended with structs. With a class, atomicLoad only loads the class reference.

Sure, for classes it doesn't provide anything, the example above wasn't too useful.

Keep in mind that atomic loading a whole struct is rather rare, since at most you can load 2 * size_t.sizeof.
On most architectures other than x86, you can load only a single size_t.
(for easy ref: https://github.com/tc39/ecmascript_sharedmem/blob/master/MACHINES.md)

Yes, the use-cases are mostly lock-free data structures and such. They are limited to rather simple data types, hence the wrapping approach might work.

For structs, it has more appeal. Instead of generating properties, we could also generate a struct type that has the same fields, but applies TailShared to their types:

If that still supports implicit conversion to the shared original type (e.g. for method calls), that would be fine as well. Preserving the exact struct layout (alignment) is critical for this.

Guess there isn't much that can be done to support protection attributes in such a wrapper, as we cannot add a type to the original module. But as those are only simple data types (mostly POD), it might be acceptable.

@aG0aep6G
Copy link
Contributor Author

Preserving the exact struct layout (alignment) is critical for this.

Yeah, and it's a good bit more complicated than generating getters. So I'm backing away from that idea.

@ZombineDev, @andralex: Are you on board with returning a wrapper for structs when we can't just strip shared?

This is what I have at the moment:

template TailShared2001(T)
{
    static if (is(T == struct) && is(TailShared!T == shared))
    {
        struct TailShared2001
        {
            static foreach (i, alias field; T.tupleof)
            {
                mixin("
                    @property ref " ~ __traits(identifier, field) ~ "()
                    {
                        alias R = TailShared!(typeof(field));
                        return * cast(R*) &_impl.tupleof[i];
                    }
                ");
            }
            shared(T) _impl;
            alias _impl this;
        }
    }
    else alias TailShared2001 = TailShared!T;
}
unittest
{
    struct S { int head; int* tailPointer; }
    TailShared2001!S s;
    static assert(!is(typeof(s) == shared));
    static assert(is(typeof(s.head) == int));
    static assert(is(typeof(s.tailPointer) == shared(int)*));
    static assert(!is(typeof(s) : S));
    static assert(is(typeof(s) : shared S));
}

@MartinNowak
Copy link
Member

@property ref " ~ __traits(identifier, field) ~ "()

Should be @trusted, otherwise looks good.

@aG0aep6G
Copy link
Contributor Author

I've pushed the wrapper.

As far as i can tell, private fields are not an issue, because tupleof ignores that.

One limitation of the wrapper is with code like this: &atomicLoad(s).field. Since field is a method, you now get a delegate instead of a pointer to the returned field. A struct with the same the same layout but TailShared fields wouldn't have that problem. Maybe I'll get back to that idea.

Unfortunately, this still works with the wrapper: int* p = atomicLoad(s).tailPointer;. But that's a compiler bug: issue 17769.

I.e., instead of trying to figure out if the load is supported and getting
it wrong, just ask the compiler if it works.
@MartinNowak
Copy link
Member

Since field is a method, you now get a delegate instead of a pointer to the returned field.

That's what @property is for ;), see https://run.dlang.io/is/cNC6UA.

Copy link
Member

@MartinNowak MartinNowak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thx for your work :).

}
return false;
}
while (canFind(fieldNames, name)) name ~= "_";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use __ here, they are reserved for internal (compiler&std lib) usage. To use a suffix that won't clash w/ phobos, how about __tailSharedImpl.

@dlang-bot dlang-bot merged commit 02d9fc4 into dlang:master Sep 1, 2017
When loading a class reference, `atomicLoad` now leaves the `shared` qualifier
on.

---
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spurious code block delimiter

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! Do we have a bugzilla for the missing line number in the error message "Error: unmatched --- in DDoc comment"?

@aG0aep6G
Copy link
Contributor Author

aG0aep6G commented Sep 1, 2017

Since field is a method, you now get a delegate instead of a pointer to the returned field.

That's what @Property is for ;), see https://run.dlang.io/is/cNC6UA.

That prints:

int delegate()
int delegate() @property

I.e., it shows that @property doesn't help.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug Fix Include reference to corresponding bugzilla issue
Projects
None yet
8 participants