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().