Skip to content
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

Threading fixes #76230

Merged
merged 6 commits into from
Dec 16, 2024
Merged

Threading fixes #76230

merged 6 commits into from
Dec 16, 2024

Conversation

333fred
Copy link
Member

@333fred 333fred commented Dec 2, 2024

We have a few theoretical races in some of our threading code, as detailed in #75759. I've audited our uses of ImmutableArray as backing fields for this pattern, fixing it up where I find issues, as well as a couple of other threading issues that I found while looking at things named lazy in our codebase.

Closes #75759

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Dec 2, 2024
@jaredpar jaredpar removed the untriaged Issues and PRs which have not yet been triaged by a lead label Dec 3, 2024
@@ -613,9 +613,9 @@ private static void GetExportedTypes(NamespaceOrTypeSymbol symbol, int parentInd

if (_lazyExportedTypes.IsDefault)
{
_lazyExportedTypes = CalculateExportedTypes();
var initialized = ImmutableInterlocked.InterlockedInitialize(ref _lazyExportedTypes, CalculateExportedTypes());
Copy link
Member Author

Choose a reason for hiding this comment

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

This is not a case of immutable arrays being used as a gate for other logic fields. Rather, it is a simple threading issue that I discovered while auditing all of our lazy immutable array fields. It could result in diagnostics being double reported depending on calling patterns, based on simple thread timing.

public bool IsUseSiteDiagnosticPopulated => (_bits & IsUseSiteDiagnosticPopulatedBit) != 0;
public bool IsConditionalPopulated => (_bits & IsConditionalPopulatedBit) != 0;
public bool IsOverriddenOrHiddenMembersPopulated => (_bits & IsOverriddenOrHiddenMembersPopulatedBit) != 0;
public bool IsObsoleteAttributePopulated => (Volatile.Read(ref _bits) & IsObsoleteAttributePopulatedBit) != 0;
Copy link
Member Author

Choose a reason for hiding this comment

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

For these types of flags enums, where we read a bit from packed flags, then look at a separate structure for the actual value, nothing was preventing us from looking at the separate structure first due to speculation or reordering. I did not use Volatile.Read everywhere because, for the other flags, both the "is populated" and "result" sections are both bits on the same integer, and we do not have to worry about out-of-order reads of the same field.

{
System.Diagnostics.Debug.Assert(typesByNS != null);
Copy link
Member Author

Choose a reason for hiding this comment

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

This was a simple thread-timing issue waiting to happen. Rather than using lazyTypes and lazyNamespaces as proxies to the real sentinel, we should just use _typesByNS, which is the actual sentinel that work was completed.

@@ -221,7 +222,7 @@ private void ComputeParameters()
_declarationDependencies.AddAll(diagnostics.DependenciesBag);
diagnostics.Free();
_lazyIsVarArg = isVararg;
_lazyParameters = parameters;
RoslynImmutableInterlocked.VolatileWrite(ref _lazyParameters, parameters);
Copy link
Member Author

Choose a reason for hiding this comment

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

Nothing prevents writes within a lock from being observed in alternate orders by other threads, when those other threads don't use a lock to check themselves. Since _lazyParameters is checked for Default outside of a lock as the sentinel, we therefore need to make sure that it's the last write inside this lock or we could still race.

@@ -369,11 +370,11 @@ internal int GetDisplayLineNumber(TextSpan span)
/// </summary>
internal Cci.DebugSourceInfo GetDebugSourceInfo()
{
if (_lazyChecksum.IsDefault)
if (RoslynImmutableInterlocked.VolatileRead(ref _lazyChecksum).IsDefault)
Copy link
Member Author

Choose a reason for hiding this comment

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

Stumbling on this was frankly a bit shocking. My only conclusion is that GetDebugSourceInfo can never be called from threaded code, because no speculation or reordering is required to race here. It just sets the fields in the wrong order!

@333fred 333fred marked this pull request as ready for review December 6, 2024 01:20
@333fred 333fred requested a review from a team as a code owner December 6, 2024 01:20
Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 5)

@333fred
Copy link
Member Author

333fred commented Dec 9, 2024

@dotnet/roslyn-compiler for a second review.

1 similar comment
@333fred
Copy link
Member Author

333fred commented Dec 10, 2024

@dotnet/roslyn-compiler for a second review.

@333fred 333fred enabled auto-merge (squash) December 16, 2024 18:13
@333fred 333fred merged commit 1715a86 into dotnet:main Dec 16, 2024
28 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Dec 16, 2024
@333fred 333fred deleted the threading branch December 17, 2024 00:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Theoretical race condition in PETypeParameterSymbol (and possibly other locations)
4 participants