What is `Task.Run()` vs `Task.Factory.StartNew()`?

4 minintermediate.NETTaskthread-pool

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:

AspectTask.Run()Task.Factory.StartNew()
SimplicitySimple, recommendedComplex, more options
Default SchedulerThreadPool (TaskScheduler.Default)Can specify custom scheduler
Async LambdaAutomatically unwrapsReturns Task<Task<T>> - needs Unwrap()
Task OptionsLimited optionsFull TaskCreationOptions
Long-RunningNot idealSupports LongRunning option
When to Use99% of scenariosCustom schedulers, long-running tasks

Recommendation: Use Task.Run() unless you specifically need the advanced features of Task.Factory.StartNew().