Task vs Thread difference in .NET

The difference between Tasks and Threads in .NET. Plain and simple.

Task vs Thread difference in .NET
Abstract vs. Concrete

Task is an abstraction.

Thread is a concrete mechanism.

That's it, period. Read again a few more times and remember!


What does that mean?

Well, the Task API proposes an abstract model that accommodates potential delayed-return execution. That simple. At its core, it doesn't impose anything on how the concrete implementation of that execution looks like.

Threads on the other hand, are a common, well-known, concrete mechanism that enable parallel code execution. They are baked into any modern operating system (Windows, Mac, Linux, Unix, Android, iOS, etc.). I'll not expand on them as there's tons of good, existing articles about threads (https://www.google.com/search?q=os+threads).

Here's a visual representation for easier understanding:

Task is an abstraction that can have a concrete implementation based on threads

That being said, wrongfully thinking they're one and the same, comes from the fact that most practical use cases of Tasks are based on threads.

But that's not always the case and when it is we're actually just aligning thread programming syntax with the Task abstractions syntax.

Task.Run(() => {}) //will launch a new thread under the hood
Task.Delay(0) //will  NOT launch a new thread
https://github.com/microsoft/referencesource/blob/51cf7850defa8a17d815b4700b67116e3fa283c2/mscorlib/system/threading/Tasks/Task.cs#L5878

The above examples have the same usage syntax, as they both return a Task instance, yet one of them spawns a new thread while the other one doesn't. For the consumer code... it's all the same.


With such an abstraction in place, .NET builds on top of it the well-known and blindly-used async/await sugar syntax, which, thanks to the Task abstract model, will compile into a state machine that massively simplifies the intensive effort of synchronizing delayed execution.

Expanding on this topic is a whole different article. For now it's enough to know that the Task abstraction enables the async/await syntax; which, needless to say, works regardless of the underlying implementation (with threads or without threads).

Therefore our own code becomes a lot cleaner, lighter and easier to read and maintain.

But again, remember:

Task == abstraction.

Thread == concrete mechanism.


Below, I wrote a couple of snippets that exemplify these concepts:

Full code on my GitHub repo: https://github.com/hinteadan/h-task-vs-thread-in-dotnet

internal class TaskWithImplicitThread : ImATaskExample
    {
        public Task ExecuteTaskExample()
        {
            return
                Task.Run(() =>
                {
                    Thread.CurrentThread.Name = "[Task.Run generated Thread]";
                    Console.WriteLine($"I'm a piece of code running on thread {Thread.CurrentThread.ManagedThreadId} - {Thread.CurrentThread.Name}");
                });
        }
    }
Task that starts a thread implicitly via Task.Run
internal class TaskWithExplicitThread : ImATaskExample
    {
        public Task ExecuteTaskExample()
        {
            TaskCompletionSource taskCompletionSource = new TaskCompletionSource();

            Thread explicitThread = new Thread(() =>
            {
                Thread.CurrentThread.Name = "[new Thread generated Thread]";
                Console.WriteLine($"I'm a piece of code running on thread {Thread.CurrentThread.ManagedThreadId} - {Thread.CurrentThread.Name}");
                taskCompletionSource.SetResult();
            });

            explicitThread.Start();

            return taskCompletionSource.Task;
        }
    }
Task that starts a thread explicitly
internal class TaskDoingNothing : ImATaskExample
    {
        public Task ExecuteTaskExample()
        {
            Console.WriteLine($"I'm a piece of code running on thread {Thread.CurrentThread.ManagedThreadId}");
            return Task.CompletedTask;
        }
    }
Task that does nothing, starts no threads
internal class TaskWithExternalProcess : ImATaskExample
    {
        public Task ExecuteTaskExample()
        {
            TaskCompletionSource taskCompletionSource = new TaskCompletionSource();

            Process.Start(new ProcessStartInfo
            {
                UseShellExecute = false,
                RedirectStandardOutput = true,
                Arguments = "/c echo I'm the standard output of the external process",
                CreateNoWindow = true,
                FileName = "cmd.exe",
            })
            .And(process =>
            {
                process.EnableRaisingEvents = true;
                process.Exited += (s, e) => {
                    Console.WriteLine(process.StandardOutput.ReadToEnd());
                    Console.WriteLine($"I'm a piece of code running on thread {Thread.CurrentThread.ManagedThreadId}");
                    taskCompletionSource.SetResult();
                };
            });

            return taskCompletionSource.Task;
        }
    }
Task that starts and external process and resolves when the external process has exited
internal class TaskWithUserInput : ImATaskExample
    {
        public Task ExecuteTaskExample()
        {
            TaskCompletionSource<string> taskCompletionSource = new TaskCompletionSource<string>();

            Console.WriteLine("Waiting for some user input, type something and press [ENTER]:");
            string userInput = Console.ReadLine();

            Console.WriteLine($"User typed: {userInput}");
            Console.WriteLine($"I'm a piece of code running on thread {Thread.CurrentThread.ManagedThreadId}");
            taskCompletionSource.SetResult(userInput);

            return taskCompletionSource.Task;
        }
    }
Task that waits for user input and resolves afterwards
internal class TaskWithTimer : ImATaskExample
    {
        public Task ExecuteTaskExample()
        {
            TaskCompletionSource taskCompletionSource = new TaskCompletionSource();
            new Timer(x => {

                Console.WriteLine("I'm timer-triggered task that ended 3 seconds after it started");
                Console.WriteLine($"I'm a piece of code running on thread {Thread.CurrentThread.ManagedThreadId}");
                taskCompletionSource.SetResult();

            }, null, TimeSpan.FromSeconds(3), Timeout.InfiniteTimeSpan);

            return taskCompletionSource.Task;
        }
    }
Task that resolves based on timer, 3 seconds after start

Full, ready to run Visual Studio solution available on my GitHub repo: https://github.com/hinteadan/h-task-vs-thread-in-dotnet