-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Avoid race condition in map for TupleElementIndex #58500
Conversation
|
||
return found is not null | ||
? tupleElementPosition - 1 | ||
: -1; |
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.
is any of this expensive? would it be worth caching the reuslt of this? #Resolved
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.
Considered this question and I lean towards not caching.
When using tuple syntax, we create TupleElementFieldSymbol
for default elements (and TupleVirtualElementFieldSymbol
for named elements) which stores location and name. So this path is only used when accessing a field directly from ValueTuple
type (from source or PE) and that'd be less common.
I've filed an issue for discussion which proposes to remove this wrapping with TupleElementFieldSymbol
. If we go down that route then caching would make more sense.
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.
cool, that makes sense to me :)
{ | ||
if (name.StartsWith("Item", StringComparison.Ordinal)) | ||
string tail = name.Substring(4); |
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.
string tail = name.Substring(4); | |
string tail = name.Substring("Item".Length); |
(will be the exact same jitted code, but this helps make it clearer what's going on (imo)). #Resolved
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.
will be the exact same jitted code
It looks like get_Length()
is called (see sharplab.io).
WellKnownMember wellKnownMember = NamedTypeSymbol.GetTupleTypeMember(arity, tupleElementPosition); | ||
MemberDescriptor descriptor = WellKnownMembers.GetDescriptor(wellKnownMember); | ||
Symbol found = CSharpCompilation.GetRuntimeMember(ImmutableArray.Create<Symbol>(this), descriptor, CSharpCompilation.SpecialMembersSignatureComparer.Instance, | ||
accessWithinOpt: null); // force lookup of public members only |
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.
Can we call comparer.MatchFieldSignature(this, descriptor.Signature)
directly instead? #ByDesign
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.
GetRuntimeMember
checks a few other things (static-ness, accessibility). I'd rather stay close to logic we used in MakeSynthesizedTupleMembers
.
/// This supports <see cref="FieldSymbol.TupleElementIndex"/>. | ||
/// For those fields, we map from their definition to an index. | ||
/// </summary> | ||
private SmallDictionary<FieldSymbol, int>? _lazyFieldDefinitionsToIndexMap; |
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.
Are we certain that nothing else in this class is prone to a similar race? Would it be simpler and more robust to ensure that members and tuple data are in sync by using an alternative approach? For example, by ensuring that they are stored together at once. #Resolved
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.
I've reviewed the other parts of TupleExtraData
and didn't spot a similar race condition.
That said, there is a race condition left somewhere (issue #53943) which seems different to the one fixed here. I haven't identified the root cause yet.
For the suggestion to have MakeSynthesizedTupleMembers
return the map and have the caller store it, we'd discussed this with Chuck among a few options before the holiday break (but we leaned towards removing the map). I'm open to the approach if Chuck is okay with it.
If we do go that route, do you have an initialization pattern for the caller? Feels like we'd need to spin-wait the threads that are waiting for the winning thread to save the map.
I think that the more robust way to avoid this and similar races would be to change |
src/Compilers/CSharp/Portable/Symbols/Tuples/TupleTypeSymbol.cs
Outdated
Show resolved
Hide resolved
This reverts commit 27d7a4b.
Symbol found = CSharpCompilation.GetRuntimeMember(ImmutableArray.Create<Symbol>(this), descriptor, CSharpCompilation.SpecialMembersSignatureComparer.Instance, | ||
accessWithinOpt: null); // force lookup of public members only |
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.
nit:
Symbol found = CSharpCompilation.GetRuntimeMember(ImmutableArray.Create<Symbol>(this), descriptor, CSharpCompilation.SpecialMembersSignatureComparer.Instance, | |
accessWithinOpt: null); // force lookup of public members only | |
Symbol found = CSharpCompilation.GetRuntimeMember( | |
ImmutableArray.Create<Symbol>(this), | |
descriptor, | |
CSharpCompilation.SpecialMembersSignatureComparer.Instance, | |
accessWithinOpt: null); // force lookup of public members only |
// ex: no "Item2" in 'ValueTuple<T1>' | ||
return -1; | ||
} | ||
Debug.Assert(tupleElementPosition < NamedTypeSymbol.ValueTupleRestPosition); |
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 clarify why we can assume this?
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 this assert be moved to immediately following the if (!ContainingType.IsDefinition) { ... }
block?
Is this PR ready to merge? |
The race condition is that
PENamedTypeSymbol.LoadMembers
callsMakeSynthesizedTupleMembers
and then saves members in_lazyMembersInDeclarationOrder
. ButMakeSynthesizedTupleMembers
is having a side-effect which saves some members in a map.If two threads call
LoadMembers
concurrently, the members in_lazyMembersInDeclarationOrder
and in_lazyFieldDefinitionsToIndexMap
may not match (ie. they are coming from two different threads that are loading symbols).Here are some of the existing tests that are relevant:
TupleWithElementNamedWithDefaultName
,TestValueTuple8Definition
,IndexOfUnderlyingFieldsInTuple9
,UnderlyingTypeMemberWithWrongSignature_1
.Fixes #52658 (flaky value of TupleElementIndex in TupleWithElementNamedWithDefaultName test)
Fixes #57560 (crash in TupleWithElementNamedWithDefaultName test now that AssertOrFailFast was added)
This PR maintains existing behavior, but I think it's not quite right. Filed #58499