Compared to a thread, a Task is higher-level abstraction—it represents a concurrent operation that might or might not be backed by a thread. Tasks are compositional (you can chain them together through the use of continuations). They can use the thread pool to lessen startup latency, and with a TaskCompletionSource, they can employ a callback approach that avoids threads altogether while waiting on I/O-bound operations.
The easiest way to start a Task backed by a thread is with the static method Task.Run (the Task class is in the System.Threading.Tasks namespace). Simply pass in an Action delegate:
Task.Run (() => Console.WriteLine ("Foo"));
Tasks use pooled threads by default, which are background threads. This means that when the main thread ends, so do any tasks that you create.
Calling Task.Run in this manner is similar to starting a thread as follows (except for the thread pooling implications that we discuss shortly):
new Thread (() => Console.WriteLine ("Foo")).Start();
Task.Run returns a Task object that we can use to monitor its progress, rather like a Thread object.You can track a task’s execution status via its Status property.
Calling Wait on a task blocks until it completes and is the equivalent of calling Join on a thread:
Task task = Task.Run (() =>
{
Thread.Sleep (2000);
Console.WriteLine ("Foo");
});
Console.WriteLine (task.IsCompleted); // False
task.Wait(); // Blocks until task is complete
Task has a generic subclass called Task< TResult >, which allows a task to emit a return value. You can obtain a Task< TResult > by calling Task.Run with a Func< TResult > delegate (or a compatible lambda expression) instead of an Action:
Task<int> task = Task.Run (() => { Console.WriteLine ("Foo"); return 3; });
// ...
You can obtain the result later by querying the Result property. If the task hasn’t yet finished, accessing this property will block the current thread until the task finishes:
int result = task.Result; // Blocks if not already finished
Console.WriteLine (result); // 3
In the following example, we create a task that uses LINQ to count the number of prime numbers in the first three million (+2) integers:
Task<int> primeNumberTask = Task.Run (() =>
Enumerable.Range (2, 3000000).Count (n =>
Enumerable.Range (2, (int)Math.Sqrt(n)-1).All (i => n % i > 0)));
Console.WriteLine ("Task running...");
Console.WriteLine ("The answer is " + primeNumberTask.Result);
Task< TResult > can be thought of as a “future,” in that it encapsulates a Result that becomes available later in time.
Unlike with threads, tasks conveniently propagate exceptions. So, if the code in your task throws an unhandled exception (in other words, if your task faults), that exception is automatically rethrown to whoever calls Wait()—or accesses the Result property of a Task< TResult >:
// Start a Task that throws a NullReferenceException:
Task task = Task.Run (() => { throw null; });
try
{
task.Wait();
}
catch (AggregateException aex)
{
if (aex.InnerException is NullReferenceException)
Console.WriteLine ("Null!");
else
throw;
}
A continuation says to a task, “when you’ve finished, continue by doing something else.” A continuation is usually implemented by a callback that executes once upon completion of an operation. There are two ways to attach a continuation to a task. The first is particularly significant because it’s used by C#’s asynchronous functions, as you’ll see soon. We can demonstrate it with the prime number counting task that we wrote a short while ago in “Returning values”:
Task<int> primeNumberTask = Task.Run (() =>
Enumerable.Range (2, 3000000).Count (n =>
Enumerable.Range (2, (int)Math.Sqrt(n)-1).All (i => n % i > 0)));
var awaiter = primeNumberTask.GetAwaiter();
awaiter.OnCompleted (() =>
{
int result = awaiter.GetResult();
Console.WriteLine (result); // Writes result
});