What is `Task.Run()` vs `Task.Factory.StartNew()`?
Quick Answer
Task.Run() is the modern, recommended way to offload work to the thread pool — it defaults to safe options and unwraps returned tasks automatically. Task.Factory.StartNew() is older and more configurable but has dangerous defaults (it doesn't unwrap async delegates and may not use the thread pool), so it requires extra flags like TaskScheduler.Default and Unwrap(). Use Task.Run() unless you specifically need StartNew's advanced options.
Detailed Answer
Task.Run() is the simpler, modern method for starting a task. It's the recommended approach for most scenarios.
Task.Factory.StartNew() provides more control and configuration options but is more complex and has some gotchas.
Example:
public class TaskCreationComparison
{
// Task.Run() - Simple and recommended
public async Task RunWithTaskRunAsync()
{
// Task.Run() always uses TaskScheduler.Default (ThreadPool)
var result = await Task.Run(() =>
{
Console.WriteLine($"Task.Run on thread: {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
return 42;
});
Console.WriteLine($"Result: {result}");
}
// Task.Run() with async lambda
public async Task RunWithAsyncLambdaAsync()
{
// Task.Run properly unwraps async lambdas
var result = await Task.Run(async () =>
{
await Task.Delay(1000);
return "Completed";
});
Console.WriteLine(result);
}
// Task.Factory.StartNew() - More control but complex
public async Task RunWithFactoryStartNewAsync()
{
// Requires explicit unwrapping for async lambdas
var task = Task.Factory.StartNew(() =>
{
Console.WriteLine($"Factory.StartNew on thread: {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
return 42;
});
var result = await task;
Console.WriteLine($"Result: {result}");
}
// Task.Factory.StartNew() with options
public async Task RunWithOptionsAsync()
{
var task = Task.Factory.StartNew(
() =>
{
Console.WriteLine("Long-running task started");
Thread.Sleep(5000);
return "Done";
},
CancellationToken.None,
TaskCreationOptions.LongRunning, // Creates dedicated thread
TaskScheduler.Default
);
var result = await task;
Console.WriteLine(result);
}
// Demonstrating the async lambda gotcha with Factory.StartNew
public async Task DemonstrateGotchaAsync()
{
// WRONG: This returns Task<Task>, not Task
Task<Task> outerTask = Task.Factory.StartNew(async () =>
{
await Task.Delay(1000);
return "Result";
});
// Need to unwrap manually
Task innerTask = await outerTask;
string result = await innerTask;
// OR use Unwrap()
var unwrappedTask = Task.Factory.StartNew(async () =>
{
await Task.Delay(1000);
return "Result";
}).Unwrap();
result = await unwrappedTask;
// Task.Run handles this automatically - CORRECT WAY
result = await Task.Run(async () =>
{
await Task.Delay(1000);
return "Result";
});
}
}
// Practical examples showing when to use each
public class PracticalExamples
{
// Use Task.Run for most scenarios
public async Task<List> ProcessDataAsync(List data)
{
// Offload CPU-intensive work to thread pool
return await Task.Run(() =>
{
return data.Select(x => x * x).ToList();
});
}
// Use Task.Factory.StartNew for long-running tasks
public Task StartBackgroundServiceAsync()
{
return Task.Factory.StartNew(
() =>
{
while (true)
{
// Long-running background work
Console.WriteLine("Service running...");
Thread.Sleep(5000);
}
},
TaskCreationOptions.LongRunning // Gets dedicated thread
);
}
// Use Task.Factory.StartNew with custom scheduler
public async Task RunWithCustomSchedulerAsync()
{
var scheduler = new CustomTaskScheduler();
var task = Task.Factory.StartNew(
() =>
{
Console.WriteLine("Running with custom scheduler");
return 100;
},
CancellationToken.None,
TaskCreationOptions.None,
scheduler
);
var result = await task;
Console.WriteLine($"Result: {result}");
}
}
// Custom task scheduler example
public class CustomTaskScheduler : TaskScheduler
{
protected override IEnumerable GetScheduledTasks()
{
return Enumerable.Empty();
}
protected override void QueueTask(Task task)
{
ThreadPool.QueueUserWorkItem(_ => TryExecuteTask(task));
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return TryExecuteTask(task);
}
}
Key Differences:
| Aspect | Task.Run() | Task.Factory.StartNew() |
|---|---|---|
| Simplicity | Simple, recommended | Complex, more options |
| Default Scheduler | ThreadPool (TaskScheduler.Default) | Can specify custom scheduler |
| Async Lambda | Automatically unwraps | Returns Task<Task<T>> - needs Unwrap() |
| Task Options | Limited options | Full TaskCreationOptions |
| Long-Running | Not ideal | Supports LongRunning option |
| When to Use | 99% of scenarios | Custom schedulers, long-running tasks |
Recommendation: Use Task.Run() unless you specifically need the advanced features of Task.Factory.StartNew().