Task.Run: when it helps and when it doesn't
Task.Run queues a delegate on the thread pool, where it runs on another thread. await Task.Run(...) frees the calling thread until that work finishes.
int result = await Task.Run(() => DoHeavyMath());Why it is for CPU-bound work
await alone does not move work anywhere. It frees a thread only when the awaited operation is a real wait, such as I/O. A method with no await, or await Task.FromResult(DoHeavyMath()), runs DoHeavyMath() on the caller’s thread. CPU-bound work has to be handed to another thread, and Task.Run does that.
async Task<int> ComputeAsync()
{
int result = DoHeavyMath(); // pure CPU work: no I/O, no await
return result;
}
When it helps: the UI thread
A UI app has one thread for input and painting. Heavy work on that thread freezes the UI. Task.Run moves the work to a pool thread, and the code after the await resumes on the UI thread (see the UI threadThe UI thread and SynchronizationContextOne thread owns the controls, and other threads post work to it.Opens in a new tab).
async void Button_Click(object sender, EventArgs e)
{
label.Text = "working...";
int result = await Task.Run(() => DoHeavyMath()); // offload to a pool thread
label.Text = result.ToString(); // back on the UI thread automatically
}
When it helps: independent chunks
Several independent CPU jobs can run at the same time, each in its own Task.Run, awaited together with Task.WhenAll. Parallel.ForEach is the usual form for a large collection (see fan-out and fan-inFan-out and fan-in with Parallel.ForRender independent items on all cores, keep the results in order, then merge them one at a time.Opens in a new tab).
// Run independent CPU-bound tasks in parallel, and await them all
var tasks = items.Select(item => Task.Run(() => Process(item)));
var results = await Task.WhenAll(tasks);
// Or, synchronous data parallelism across cores:
Parallel.ForEach(items, item => Process(item));
When it doesn’t
In a server request, the handler already runs on a pool thread. Task.Run there needs a second pool thread for the same computation and adds scheduling overhead, so a single computation should just run directly. Wrapping I/O in Task.Run gains nothing either: it holds a thread to wait for something that never needed one.
Where it matters
Two different problems get mixed up. await on I/O frees a thread from waiting. Task.Run moves CPU work off a thread that has to stay free, or runs several CPU jobs at once.