-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Thread-Safe Tasks spec #12111
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
Merged
Merged
Thread-Safe Tasks spec #12111
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
e762eae
Add api reference document 1st version.
AR-May 2f31d7b
Address review comments, fix typos
AR-May 0914b4e
Add first draft for IThreadSafeTask interface.
AR-May 6415ea9
address comments; introduce AbsolutePath classes
AR-May a2df50f
Process API changes
AR-May 0697dd9
Add more options, address review comments.
AR-May 69dfc1d
Add some questions, minor refactoring
AR-May cab9556
small fixes
AR-May 0dba237
Rename directory
AR-May 349e849
Merge branch 'main' into dev/AR-May/thread-safe-tasks-spec
AR-May 6c2b55e
Add more details about APIs
AR-May ac5fffa
small change of wordings
AR-May 52243b2
Make absolute and relative structs read-only and prohibit ToAbsoluteP…
AR-May 9e0a088
Apply review suggestions
AR-May 03e40af
naming changes and small wording changes
AR-May 4135f9d
small wording changes
AR-May 0a6c100
rename ProjectDirectory -> ProjectCurrentDirectory
AR-May 9170036
mark API reference a draft
AR-May 5b154f3
rename ProjectCurrentDirectory -> ProjectDirectory and update access …
AR-May b23daf7
Merge branch 'main' into dev/AR-May/thread-safe-tasks-spec
AR-May File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
154 changes: 154 additions & 0 deletions
154
documentation/specs/MultithreadedMSBuild/thread-safe-tasks-api-reference.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| # Thread-Safe Tasks: API Analysis Reference | ||
|
|
||
| This document provides a list of .NET APIs that should not be used or should be used with caution in thread-safe tasks. These APIs are problematic because they either rely on or modify process-level state, which can cause race conditions in multithreaded execution. | ||
|
|
||
| The APIs listed in this document will be detected by Roslyn analyzers and/or MSBuild BuildCheck to help identify potential threading issues in tasks that implement `IThreadSafeTask`. | ||
|
|
||
| **Note**: The analyzers rely on **static code analysis** and may not catch all dynamic scenarios (such as reflection-based API calls). | ||
|
|
||
| ## API Issues Categories | ||
|
|
||
| Categories of threading issues with .NET API usage in thread-safe tasks to be aware of: | ||
|
|
||
| 1. **Working Directory Modifications and Usage**, such as file system operations with relative paths. | ||
| 1. **Environment Variables Modification and Usage** | ||
| 1. **Process Culture Modification and Usage**, which can affect data formatting. | ||
| 1. **Assembly Loading** | ||
| 1. **Static Fields** | ||
|
|
||
| **TODO**: Consider other possible causes of issues: | ||
| 1. Creating new AppDomain. | ||
| 1. App config changes. | ||
|
|
||
| ### Best Practices | ||
|
|
||
| Instead of the problematic APIs listed below, thread-safe tasks should: | ||
|
|
||
| 1. **Use `ITaskExecutionContext`** for all file system operations, environment variable changes, and working directory changes. | ||
| 1. **Always use absolute paths** when still using some standard .NET file system APIs. | ||
| 1. **Explicitly configure external processes** with working directory and environment variables. | ||
| 1. **Never modify process culture**: Avoid modifying culture defaults. | ||
| ### Additional Considerations | ||
|
|
||
| ## Detailed API Reference | ||
|
|
||
| The following tables list specific .NET APIs and their threading safety classification: | ||
|
|
||
| ### System.IO.Path Class | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | `Path.GetFullPath(string path)` | ERROR | Uses current working directory | Use MSBuild API | | ||
|
|
||
| **TODO**: Check other methods and exclude any indirect dependency on working directory. | ||
|
|
||
| ### System.IO.File Class | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | All methods | ERROR | Uses current working directory | Use absolute paths | | ||
|
|
||
| ### System.IO.Directory Class | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | All methods | ERROR | Uses current working directory | Use absolute paths | | ||
|
|
||
| ### System.Environment Class | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | All properties setters | ERROR | Modifies process-level state | Use MSBuild API | | ||
| | `Environment.CurrentDirectory` (getter, setter) | ERROR | Accesses process-level state | Use MSBuild API | | ||
| | `Environment.Exit(int exitCode)` | ERROR | Terminates entire process | Return false from task or throw exception | | ||
| | `Environment.FailFast` all overloads | ERROR | Terminates entire process | Return false from task or throw exception | | ||
| | All other methods | ERROR | Modifies process-level state | Use MSBuild API | | ||
|
|
||
| ### System.IO.FileInfo Class | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | Constructor `FileInfo(string fileName)` | WARNING | Uses current working directory | Use absolute paths | | ||
|
AR-May marked this conversation as resolved.
|
||
| | `CopyTo` all overloads | WARNING | Destination path relative to current directory | Use absolute paths | | ||
| | `MoveTo` all overloads | WARNING | Destination path relative to current directory | Use absolute paths | | ||
| | `Replace` all overloads | WARNING | Paths relative to current directory | Use absolute paths | | ||
|
|
||
| ### System.IO.DirectoryInfo Class | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | Constructor `DirectoryInfo(string path)` | WARNING | Uses current working directory | Use absolute paths | | ||
| | `MoveTo(string destDirName)` | WARNING | Destination path relative to current directory | Use absolute paths | | ||
|
|
||
| ### System.IO.FileStream Class | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | Constructor `FileStream` all overloads | WARNING | Uses current working directory | Use absolute paths | | ||
|
|
||
| ### System.IO Stream Classes | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | Constructor `StreamReader` all overloads | WARNING | Uses current working directory | Use absolute paths | | ||
|
|
||
| ### System.Diagnostics.Process Class | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | All properties setters | ERROR | Modifies process-level state | Avoid | | ||
| | `Process.GetCurrentProcess().Kill()` | ERROR | Terminates entire process | Avoid | | ||
| | `Process.GetCurrentProcess().Kill(bool entireProcessTree)` | ERROR | Terminates entire process | Avoid | | ||
| | `Process.Start` all overloads | ERROR | May inherit process state | Use MSBuild API | | ||
|
|
||
| ### System.Diagnostics.ProcessStartInfo Class | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | Constructor `ProcessStartInfo()` all overloads | ERROR | May inherit process state | Use MSBuild API | | ||
|
|
||
| ### System.Threading.ThreadPool Class | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | `ThreadPool.SetMinThreads(int workerThreads, int completionPortThreads)` | ERROR | Modifies process-wide settings | Avoid | | ||
| | `ThreadPool.SetMaxThreads(int workerThreads, int completionPortThreads)` | ERROR | Modifies process-wide settings | Avoid | | ||
|
|
||
| ### System.Globalization.CultureInfo Class | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | `CultureInfo.DefaultThreadCurrentCulture` (setter) | ERROR | Affects new threads | Modify the thread culture instead | | ||
| | `CultureInfo.DefaultThreadCurrentUICulture` (setter) | ERROR | Affects new threads | Modify the thread culture instead | | ||
|
|
||
| ### Static | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | Static fields | WARNING | Shared across threads, can cause race conditions | Avoid | | ||
|
AR-May marked this conversation as resolved.
|
||
|
|
||
| ### Assembly Loading (System.Reflection.Assembly class, System.Activator class) | ||
| Tasks that load assemblies dynamically in the task host may cause version conflicts. Version conflicts in task assemblies will cause build failures (previously these might have been sporadic). Both dynamically loaded dependencies and static dependencies can cause issues. | ||
|
AR-May marked this conversation as resolved.
|
||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | `Assembly.LoadFrom(string assemblyFile)` | WARNING | May cause version conflicts | Be aware of potential conflicts, use absolute paths | | ||
| | `Assembly.LoadFile(string path)` | WARNING | May cause version conflicts | Be aware of potential conflicts | | ||
| | `Assembly.Load` all overloads | WARNING | May cause version conflicts | Be aware of potential conflicts | | ||
| | `Assembly.LoadWithPartialName(string partialName)` | WARNING | May cause version conflicts | Be aware of potential conflicts | | ||
| | `Activator.CreateInstanceFrom(string assemblyFile, string typeName)` | WARNING | May cause version conflicts | Be aware of potential conflicts | | ||
| | `Activator.CreateInstance(string assemblyName, string typeName)` | WARNING | May cause version conflicts | Be aware of potential conflicts | | ||
| | `AppDomain.Load` all overloads | WARNING | May cause version conflicts | Be aware of potential conflicts | | ||
| | `AppDomain.CreateInstanceFrom(string assemblyFile, string typeName)` | WARNING | May cause version conflicts | Be aware of potential conflicts | | ||
| | `AppDomain.CreateInstance(string assemblyName, string typeName)` | WARNING | May cause version conflicts | Be aware of potential conflicts | | ||
|
|
||
| ### P/Invoke | ||
|
|
||
| **Concerns**: | ||
| - P/Invoke calls may use process-level state like current working directory | ||
| - Native code may not be thread-safe | ||
| - Native APIs may modify global process state | ||
|
|
||
| | API | Level | Short Reason | Recommendation | | ||
| |-----|-------|--------------|-------| | ||
| | `[DllImport]` attribute | WARNING | Not covered by analyzers | Review for thread safety, use absolute paths | | ||
171 changes: 171 additions & 0 deletions
171
documentation/specs/MultithreadedMSBuild/thread-safe-tasks.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| # Thread-Safe Tasks | ||
|
|
||
| ## Overview | ||
|
|
||
| MSBuild's current execution model assumes that tasks have exclusive control over the entire process during execution. This allows tasks to freely modify global process state such as environment variables, the current working directory, and other process-level resources. This design works well for MSBuild's approach of executing builds in separate processes for parallelization. With the introduction of multithreaded execution within a single MSBuild process, multiple tasks can now run concurrently. This requires a new task design to ensure thread safety when multiple tasks access shared process state simultaneously. | ||
|
|
||
| To enable this multithreaded execution model, we introduce the `IThreadSafeTask` interface that tasks can implement to declare their thread-safety capabilities. Tasks implementing this interface must avoid using APIs that modify or depend on global process state, as this could cause conflicts when multiple tasks execute concurrently. See [Thread-Safe Tasks API Reference](thread-safe-tasks-api-reference.md) for detailed guidelines. At the same time, the `IThreadSafeTask` interface provides access to the `ExecutionContext` property that allows safe access to global process state. For example, task authors should use `ExecutionContext.GetAbsolutePath(relativePath)` instead of the standard `Path.GetFullPath(relativePath)` to ensure correct path resolution. | ||
|
|
||
| ## Design | ||
|
|
||
| ```csharp | ||
| public interface IThreadSafeTask : ITask | ||
| { | ||
| ITaskExecutionContext ExecutionContext { get; set; } | ||
| } | ||
|
AR-May marked this conversation as resolved.
Outdated
|
||
| ``` | ||
|
|
||
| **Note:** Consider backporting the `IThreadSafeTask` interface to the 17.14 branch to allow a graceful fail when tasks attempt to use it. | ||
|
|
||
| The execution context provides essential methods for thread-safe task execution, replacing direct access to global process state: | ||
|
|
||
| ```csharp | ||
| public interface ITaskExecutionContext | ||
| { | ||
| AbsolutePath CurrentDirectory { get; set; } | ||
|
|
||
| AbsolutePath GetAbsolutePath(string path); | ||
|
|
||
| string? GetEnvironmentVariable(string name); | ||
| IReadOnlyDictionary<string, string> GetEnvironmentVariables(); | ||
| void SetEnvironmentVariable(string name, string? value); | ||
|
|
||
| ProcessStartInfo GetProcessStartInfo(); | ||
| Process StartProcess(ProcessStartInfo startInfo); | ||
| Process StartProcess(string fileName); | ||
| Process StartProcess(string fileName, IEnumerable<string> arguments); | ||
| } | ||
| ``` | ||
|
|
||
| **Note:** The `ITaskExecutionContext` will not be thread-safe for performance reasons. Task authors who spawn multiple threads within their task implementation must provide their own synchronization when accessing the execution context from multiple threads. However, each thread node has its own isolated context object provided to the tasks, so task authors do not need to worry about synchronization with other tasks running concurrently in different thread nodes. | ||
|
|
||
| To help task authors avoid thread-safety issues related to path handling, we introduce `AbsolutePath` and `RelativePath` classes that are implicitly convertible to string. | ||
|
|
||
| ```csharp | ||
| public class AbsolutePath | ||
|
AR-May marked this conversation as resolved.
Outdated
|
||
| { | ||
| public string Path { get; } | ||
|
|
||
| // Will be banned for use in tasks by analyzers | ||
| public AbsolutePath(string path) { } | ||
|
|
||
| public AbsolutePath(string path, AbsolutePath basePath) { } | ||
|
|
||
| public static implicit operator string(AbsolutePath path) { } | ||
|
AR-May marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| public class RelativePath | ||
| { | ||
| public string Path { get; } | ||
|
|
||
| public RelativePath(string path) { } | ||
|
|
||
| public AbsolutePath ToAbsolute(AbsolutePath? basePath = null) { } | ||
|
|
||
| public static implicit operator string(RelativePath path) { } | ||
| } | ||
| ``` | ||
|
|
||
| **Note:** Usage of `AbsolutePath` and `RelativePath` may need adjustment if similar concepts are implemented in the standard .NET API in the future. | ||
|
|
||
| **TODO:** Consider having conversions for `ITaskItem`. | ||
|
|
||
| ### Interface Evolution Strategy | ||
|
|
||
| To handle future updates without breaking existing implementations, we will create versioned interfaces: | ||
|
|
||
| ```csharp | ||
| public interface ITaskExecutionContext2 : ITaskExecutionContext | ||
| { | ||
| // New methods for version 2 | ||
| } | ||
| ``` | ||
|
|
||
| #### Alternative: Abstract Classes | ||
|
|
||
| Alternatively, we can use abstract classes: | ||
|
|
||
| ```csharp | ||
| public interface IThreadSafeTask : ITask | ||
| { | ||
| TaskExecutionContext ExecutionContext { get; set; } | ||
| } | ||
|
|
||
| public abstract class TaskExecutionContext | ||
| { | ||
| // Same methods as the interface, with default implementations | ||
| } | ||
| ``` | ||
|
|
||
| **Note:** | ||
| - Default implementations allow backward compatibility for customers who extend the class. | ||
| - However, version compatibility checking is not possible with this design. | ||
|
|
||
| ## Authoring Thread-Safe Tasks | ||
|
|
||
| ### Supporting Running 'classic' MSBuild Tasks in Thread Node | ||
|
|
||
| Task authors implementing `IThreadSafeTask` need to: | ||
|
|
||
| - Update package dependencies to support thread-safe APIs | ||
| - Create and maintain both thread-safe and legacy versions if support for older MSBuild versions is necessary | ||
| - Implement logic to choose appropriate task implementations based on MSBuild capabilities | ||
|
|
||
| These requirements effectively cut off support for a long tail of MSBuild and IDE versions, creating a barrier to adoption. | ||
|
|
||
| To allow easier adoption, MSBuild will enable the thread worker node to either recognize and wrap legacy tasks in thread-based semantics or run the 'classic' tasks themselves. This will allow task authors who are confident their tasks don't perform stateful actions to participate in multithreaded builds without sacrificing legacy support or needing to support multiple versions. | ||
|
|
||
| Legacy tasks can run in multithreaded builds if they: | ||
| - Do not change environment variables or current working directory | ||
| - Do not depend on relative path resolution from current working directory | ||
| - Do not use static fields or other shared state unsafely | ||
| - Do not rely on specific environment variable values being set | ||
|
|
||
| Task authors and users who would like to utilize this capability will need to add the `ThreadSafe` attribute to the task declaration: | ||
| ```xml | ||
| <UsingTask TaskName="MyTask" AssemblyFile="MyTask.dll" ThreadSafe="true" Condition="'$(MSBuildSupportsThreadSafeTasks)' != 'true'" /> | ||
|
AR-May marked this conversation as resolved.
Outdated
|
||
| ``` | ||
|
|
||
| **Note** We will need to service supported branches so that older versions of MSBuild ignore the `ThreadSafe` attribute and do not throw the `MSB4066` error. For MSBuild versions that would not be serviced, task authors will need to conditionally import the file with task declarations based on the running MSBuild version. | ||
|
|
||
| **Note** Alternatively, we can introduce new types of assembly factories (`AssemblyThreadCompatibleTaskFactory` and others) to avoid the error. The task declarations still need to be conditional, but this would spare us the need to service older versions. | ||
|
|
||
| This support would be enabled by default for the first release of Multithreaded MSBuild, but the goal should be to deprecate it. We will disable the compatibility bridge by default eventually. We will use build-level telemetry to track 'classic' MSBuild tasks running in multithreaded mode to monitor `IThreadSafeTask` adoption trends. | ||
|
|
||
| ## Examples | ||
| Basic `IThreadSafeTask` Example: | ||
| ```csharp | ||
| public class MyTask : IThreadSafeTask | ||
| { | ||
| public bool Execute() | ||
| { | ||
| // Use APIs provided by ExecutionContext | ||
| string envVar = ExecutionContext.GetEnvironmentVariable("EnvVar"); | ||
|
|
||
| // Convert string properties to strongly-typed paths and use them in standard File/Directory APIs | ||
| AbsolutePath path = ExecutionContext.GetAbsolutePath("SomePath"); | ||
| string content = File.ReadAllText(path); | ||
|
|
||
| return true; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Conditional Task Declaration Example: | ||
| ```xml | ||
| <Project> | ||
| <PropertyGroup> | ||
| <MSBuildSupportsThreadSafeTasks Condition="'$(MSBuildVersion)' >= '17.15'">true</MSBuildSupportsThreadSafeTasks> | ||
| </PropertyGroup> | ||
|
|
||
| <UsingTask | ||
| TaskName="MyTask" | ||
| AssemblyFile="$(MSBuildThisFileDirectory)../lib/netstandard2.0/ThreadSafe/MyTask.dll" | ||
| Condition="'$(MSBuildSupportsThreadSafeTasks)' == 'true'" /> | ||
|
AR-May marked this conversation as resolved.
Outdated
|
||
|
|
||
| <UsingTask | ||
| TaskName="MyTask" | ||
| AssemblyFile="$(MSBuildThisFileDirectory)../lib/netstandard2.0/Legacy/MyTask.dll" | ||
| Condition="'$(MSBuildSupportsThreadSafeTasks)' != 'true'" /> | ||
| </Project> | ||
| ``` | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.