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