Avoid race condition in map for TupleElementIndex #58500
Conversation
|
|
||
| return found is not null | ||
| ? tupleElementPosition - 1 | ||
| : -1; |
There was a problem hiding this comment.
is any of this expensive? would it be worth caching the reuslt of this? #Resolved
There was a problem hiding this comment.
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.
cool, that makes sense to me :)
| if (name.StartsWith("Item", StringComparison.Ordinal)) | ||
| { | ||
| if (name.StartsWith("Item", StringComparison.Ordinal)) | ||
| string tail = name.Substring(4); |
There was a problem hiding this comment.
| 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.
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.
Can we call comparer.MatchFieldSignature(this, descriptor.Signature) directly instead? #ByDesign
There was a problem hiding this comment.
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.
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.
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 |
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.
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.
Could you please clarify why we can assume this?
There was a problem hiding this comment.
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.LoadMemberscallsMakeSynthesizedTupleMembersand then saves members in_lazyMembersInDeclarationOrder. ButMakeSynthesizedTupleMembersis having a side-effect which saves some members in a map.If two threads call
LoadMembersconcurrently, the members in_lazyMembersInDeclarationOrderand in_lazyFieldDefinitionsToIndexMapmay 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