Skip to content

Multithreading: docs and analyzer should reflect that [MSBuildMultiThreadableTask] is the routing signal, not IMultiThreadableTask #14779

Description

@ViktorHofer

Updated 2026-08-24 — the original recommendation in this issue was wrong.
It proposed treating IMultiThreadableTask as a routing signal (option 1 below). That is not implementable: ToolTask implements IMultiThreadableTask (src/Utilities/ToolTask.cs), so honoring the interface for routing would silently opt in every ToolTask-derived task in the ecosystem — thousands of them, virtually all unmigrated and unaudited. The attribute-only check is not a bug; it is the only workable design.
The attribute is the routing signal. This issue is now about making the docs and the analyzer say so. Rewritten below.

Summary

[MSBuildMultiThreadableTask] is the routing signal. IMultiThreadableTask is the TaskEnvironment-injection signal. They do different jobs, and a task that has one without the other fails in a different way each time — silently, in both directions.

The implementation is correct. The documentation describes the opposite, and the analyzer does not flag either half-migrated shape.

Why the interface cannot be a routing signal

ToolTask implements IMultiThreadableTask:

// src/Utilities/ToolTask.cs
public abstract class ToolTask : Task, IIncrementalTask, ICancelableTask, IMultiThreadableTask

Routing on the interface would therefore declare every ToolTask subclass in existence thread-safe, without anyone having audited one of them. That is unshippable, and it is why NeedsTaskHostInMultiThreadedMode checks only the attribute.

This also means the interface is not a safety claim — it is plumbing. That is a useful property: "implements the interface, no attribute" is a meaningful state, describing a task whose path handling is migrated but whose overall thread-safety is not yet established. It gets correct path resolution while still being isolated in a sidecar. Several tasks in dotnet/arcade#17381 are deliberately in exactly that state.

The actual contract

Signal Effect Read by
[MSBuildMultiThreadableTask] routing — skip the sidecar TaskHost TaskRouter.NeedsTaskHostInMultiThreadedMode
IMultiThreadableTask TaskEnvironment injection TaskExecutionHost (if (TaskInstance is IMultiThreadableTask multiThreadableTask))

Both are needed for a fully migrated task, and MSBuild's own tasks apply both — in src/Tasks, 36 files reference IMultiThreadableTask and 34 also carry the attribute.

What the docs currently say

The thread-safe-tasks spec presents the interface as the primary mechanism and the attribute as a fallback:

  1. Interface-Based Thread-Safe Capability Declaration — Provides access to thread-safe APIs through TaskEnvironment to be used in the task code.
  2. Attribute-Based Thread-Safe Capability Declaration — Allows existing tasks to declare its ability run in multithreaded mode without code changes. It is a compatibility bridge option.

[...] Since MSBuild does not ship the attribute, customers using newer MSBuild versions should prefer the Interface-Based Thread-Safe Capability Declaration.

Following that literally — implement the interface, skip the attribute — produces zero benefit: the task still gets a sidecar TaskHost. The framing is backwards. The attribute is not a legacy bridge; it is the load-bearing signal, and the interface alone never opts a task in.

The two half-migrated shapes, and why they are silent

Attribute without interface — the dangerous one. The task runs in-process, but TaskEnvironment is never assigned, so it stays TaskEnvironment.Fallback and every GetAbsolutePath resolves against the process CWD rather than the project directory. In-proc is exactly where that is wrong. Nothing warns.

We shipped this into a branch while migrating dotnet/arcade: we applied the attribute and declared a plain public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; property. The analyzer was satisfied, the build was clean, and the entire plumbing was inert because the class did not implement the interface. It was caught by reading TaskExecutionHost, not by any diagnostic.

Interface without attribute — benign, and sometimes intentional (see above). Worth at most an opt-in Info, not a warning.

Requested changes

  1. Spec — state that [MSBuildMultiThreadableTask] is required for in-proc execution, that IMultiThreadableTask only controls TaskEnvironment injection, and that a fully migrated task applies both. Remove the "prefer the interface over the attribute" guidance; explain that the interface cannot be a routing signal because ToolTask implements it. If MultiThreadableTask (the abstract base in the spec) ships, note that deriving from it is not sufficient on its own.
  2. Analyzer — new diagnostic, Warning: has [MSBuildMultiThreadableTask] and declares a TaskEnvironment property but does not implement IMultiThreadableTask. The property will never be assigned; this is a silent correctness bug, not a perf issue.
  3. Analyzer — optional Info: implements IMultiThreadableTask without the attribute (will not run in-proc). Must be opt-in, since this is a legitimate intermediate state.

The TaskRouter class-level comment has the same inversion and is tracked separately in #14790.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions