-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Enablement of parallel project evaluation #12757
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
base: main
Are you sure you want to change the base?
Conversation
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.
Pull Request Overview
This PR attempts to enable parallel project evaluation by removing the write lock from the LoadProject method. However, this introduces a critical race condition that can cause failures in multi-threaded scenarios.
Key Changes:
- Removed the outer
_locker.EnterDisposableWriteLock()that wrapped the entire method logic - De-indented all code that was inside the lock block
- Minor code style improvements (named parameters like
nodeId: 0andisExplicitlyLoaded: true, and using??=operator)
| Project project = _loadedProjects.GetMatchingProjectIfAny(fileName, globalProperties, effectiveToolsVersion); | ||
|
|
||
| // The Project constructor adds itself to our collection, | ||
| // it is not done by us | ||
| project ??= new Project(fileName, globalProperties, effectiveToolsVersion, this); | ||
|
|
Copilot
AI
Nov 13, 2025
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.
Critical Race Condition: Removing the lock creates a check-then-act race condition
Removing the write lock from this method introduces a race condition that can cause InvalidOperationException when multiple threads attempt to load the same project concurrently.
The Problem:
- Thread A calls
GetMatchingProjectIfAny()at line 1268 (which has internal locking) - Thread B calls
GetMatchingProjectIfAny()at line 1268 (after Thread A releases its internal lock) - Both threads find no matching project
- Both threads create a new
Projectinstance at line 1272 - Thread A's
Projectconstructor eventually callsAddProject()(viaInitialize()→_renameHandler()→OnAfterRenameLoadedProject()) - Thread B's
Projectconstructor attempts to callAddProject()but now Thread A's project exists - Thread B's
AddProject()throwsInvalidOperationExceptionwith "OM_MatchingProjectAlreadyInCollection" (seeLoadedProjectCollection.AddProject()at lines 2007-2011)
Why Internal Locking Isn't Sufficient:
While LoadedProjectCollection has internal locking on individual operations (GetMatchingProjectIfAny() and AddProject() both lock _loadedProjects), these are separate lock acquisitions. The original outer write lock ensured the entire check-and-add sequence was atomic.
Solution:
The write lock needs to be retained to ensure atomicity of the check-and-create operation. If parallel project loading is desired, a different approach is needed, such as:
- Using a concurrent check-and-add pattern (try to add, if it fails because it exists, retrieve the existing one)
- Using a lock-free algorithm with compare-and-swap semantics
- Implementing a "get-or-create" method with internal locking in
LoadedProjectCollection
| Project project = _loadedProjects.GetMatchingProjectIfAny(fileName, globalProperties, effectiveToolsVersion); | |
| // The Project constructor adds itself to our collection, | |
| // it is not done by us | |
| project ??= new Project(fileName, globalProperties, effectiveToolsVersion, this); | |
| Project project; | |
| _lock.EnterWriteLock(); | |
| try | |
| { | |
| project = _loadedProjects.GetMatchingProjectIfAny(fileName, globalProperties, effectiveToolsVersion); | |
| // The Project constructor adds itself to our collection, | |
| // it is not done by us | |
| project ??= new Project(fileName, globalProperties, effectiveToolsVersion, this); | |
| } | |
| finally | |
| { | |
| _lock.ExitWriteLock(); | |
| } |
| foreach (KeyValuePair<string, string> globalProperty in GlobalProperties) | ||
| { | ||
| // We need to update the set of global properties to merge in the ProjectCollection global properties -- | ||
| // otherwise we might end up declaring "not matching" a project that actually does ... and then throw | ||
| // an exception when we go to actually add the newly created project to the ProjectCollection. | ||
| // BUT remember that project global properties win -- don't override a property that already exists. | ||
| foreach (KeyValuePair<string, string> globalProperty in GlobalProperties) | ||
| if (!globalProperties.ContainsKey(globalProperty.Key)) | ||
| { | ||
| if (!globalProperties.ContainsKey(globalProperty.Key)) | ||
| { | ||
| globalProperties.Add(globalProperty); | ||
| } | ||
| globalProperties.Add(globalProperty); | ||
| } | ||
| } |
Copilot
AI
Nov 13, 2025
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.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
Fixes #12520
Context
There's no need to use locking in the
LoadProjectmethod—it’s not required in this context.Changes Made
Taking the lock out of the equation let things run in parallel.
Testing
Existing tests.
Notes