asynchronous programming
C#
task parallel library
Task.Start
Task.Run

Regarding usage of Task.Start , Task.Run and Task.Factory.StartNew

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Task.Start, Task.Run, and Task.Factory.StartNew all begin work, but they are not interchangeable. In modern .NET code, Task.Run is usually the right default, Task.Start is rarely needed, and Task.Factory.StartNew is a more advanced tool that is easy to misuse.

Use Task.Run for the Usual Case

Task.Run queues work to the thread pool and returns a started Task. It is the simplest API when you want to run CPU-bound work in the background.

csharp
1using System;
2using System.Threading.Tasks;
3
4var task = Task.Run(() =>
5{
6    Console.WriteLine("Running on a background thread");
7});
8
9await task;

This is concise, predictable, and works well with async and await. If the delegate itself is asynchronous, Task.Run unwraps the nested task automatically:

csharp
1await Task.Run(async () =>
2{
3    await Task.Delay(100);
4    Console.WriteLine("Finished async work");
5});

That automatic unwrapping is one reason Task.Run is usually safer than StartNew for modern code.

Understand What Task.Start Actually Does

Task.Start only applies to a task that you created manually with the Task constructor. The task begins in the Created state and does nothing until you start it.

csharp
1using System;
2using System.Threading.Tasks;
3
4var task = new Task(() =>
5{
6    Console.WriteLine("Started later");
7});
8
9task.Start();
10await task;

This pattern is valid, but it is uncommon. Most code does not need a "create now, start later" lifecycle, so Task.Run is usually clearer.

It also comes with a constraint: calling Start on a task that has already started or completed throws an exception. That makes it less forgiving than higher-level task APIs.

Use Task.Factory.StartNew Only When You Need Extra Control

Task.Factory.StartNew exposes scheduler and creation options that Task.Run hides. That extra control can be useful, for example, when you want LongRunning behavior:

csharp
1using System;
2using System.Threading;
3using System.Threading.Tasks;
4
5var task = Task.Factory.StartNew(
6    () =>
7    {
8        Thread.Sleep(500);
9        Console.WriteLine("Long-running work finished");
10    },
11    CancellationToken.None,
12    TaskCreationOptions.LongRunning,
13    TaskScheduler.Default);
14
15await task;

The tradeoff is complexity. With StartNew, the default scheduler may differ from what you expect, and async delegates can produce Task<Task> if you are not careful.

csharp
1var nested = Task.Factory.StartNew(async () =>
2{
3    await Task.Delay(100);
4    return 42;
5});
6
7int result = await await nested;

That double await is a signal that Task.Run would often be a better choice.

A Practical Rule of Thumb

Use Task.Run when you want to offload ordinary work to the thread pool. Use Task.Start only if you deliberately need to construct a task first and start it later. Use Task.Factory.StartNew when you truly need scheduler control or special creation options and you understand the behavior you are opting into.

For I/O-bound APIs that already provide asynchronous methods, you often should not use any of these. If you already have HttpClient.GetAsync, FileStream.ReadAsync, or database async APIs, await them directly instead of wrapping them in Task.Run.

Common Pitfalls

The biggest mistake is using Task.Factory.StartNew with an async lambda and then forgetting that the result may be nested as Task<Task>.

Another issue is assuming Task.Run and StartNew always use the same scheduler behavior. They do not. Task.Run targets the default thread-pool scheduler, while StartNew can inherit the current scheduler if you do not specify one.

People also reach for Task.Run to make already-async I/O code "more async". That usually adds overhead without improving responsiveness.

Finally, Task.Start is often overused for code that could simply be written as Task.Run(...), which is shorter and less error-prone.

Summary

  • 'Task.Run is the usual choice for running background work in modern .NET code.'
  • 'Task.Start only applies to tasks created with the Task constructor and is rarely necessary.'
  • 'Task.Factory.StartNew offers more control but is easier to misuse.'
  • Be careful with async lambdas passed to StartNew, because they can create nested tasks.
  • Prefer native async APIs over wrapping I/O work in background tasks.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.