Wednesday, 26 March 2014

Task Library: Common Patterns

Creating a new task

void Main()
{
    Console.WriteLine ("I'm the main thread, starting a new task. My ID: " + Thread.CurrentThread.ManagedThreadId);    
    
    //the task is started immediately
    Task t = Task.Factory.StartNew(() => 
    { 
        Console.WriteLine ("I'm a task running concurrently with the thread that started me. My ID: "
                                + Thread.CurrentThread.ManagedThreadId);
    });
    
    Console.WriteLine ("I'm going away. My ID: " + Thread.CurrentThread.ManagedThreadId);
}



I'm the main thread, starting a new task. My ID: 20
I'm going away. My ID: 20
I'm a task running concurrently with the thread that started me. My ID: 24


The method Task.Factory.StartNew() creates a task and immediately starts it. If we want to control when a task starts, we can instantiate a new Task object and call Start() on it.

For e.g.

void Main()
{
    Console.WriteLine ("I'm the main thread, starting a new task. My ID: " + Thread.CurrentThread.ManagedThreadId);
    
    //define task
    Task t = new Task(() => 
    { 
        Console.WriteLine ("I'm a task running concurrently with the thread that started me. My ID: " 
                + Thread.CurrentThread.ManagedThreadId);
    });
    
    //start the task
    t.Start();
    
    Console.WriteLine ("I'm going away. My ID: " + Thread.CurrentThread.ManagedThreadId);
}

Waiting for a task
We can see above, that the main thread doesn’t wait for the task to complete. The task may not even start when main thread is done. The main thread with ID:20 exits before the Task thread ID:24 gets to work.

The application doesn’t wait for the Task to complete because the thread which runs the Task is a background thread. To wait on a thread we use the Wait() method on the Task object.

void Main()
{
    Console.WriteLine ("I'm the main thread, starting a new task. My ID: " + Thread.CurrentThread.ManagedThreadId);
    
    Task t = Task.Factory.StartNew(() => 
    { 
        Thread.Sleep(5000);
        Console.WriteLine ("I'm a task running concurrently with the thread that started me. My ID: " 
                + Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine ("Task: I'm going away after waking up. My ID: " + Thread.CurrentThread.ManagedThreadId);
    });
    
    t.Wait();
    
    Console.WriteLine ("Main: I'm going away after waiting. My ID: " + Thread.CurrentThread.ManagedThreadId);
}




I'm the main thread, starting a new task. My ID: 22
I'm a task running concurrently with the thread that started me. My ID: 8
Task: I'm going away after waking up. My ID: 8
Main: I'm going away after waiting. My ID: 22


Passing parameter


There are overloads available in Task.Factory.StartNew() and “new Task()” constructor that take in a parameter of type object. Let’s see an example. In the previous example the Task slept for a fixed 5 seconds. Let us change it so that the Main thread can pass in the seconds to sleep.

void Main()
{
    Console.WriteLine ("I'm the main thread, starting a new task. My ID: " + Thread.CurrentThread.ManagedThreadId);
    
    Task t = Task.Factory.StartNew((sleepingTime) => //using Action<object>
    {
        Console.WriteLine ("I'm a task running concurrently with the thread that started me. My ID: " 
                + Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine ("Sleeping for " + sleepingTime);
        Thread.Sleep(Convert.ToInt32(sleepingTime));
        Console.WriteLine ("Task: I'm going away after waking up. My ID: " + Thread.CurrentThread.ManagedThreadId);
    }, 1000); //passing the parameter value
        
    t.Wait();    
    
    Console.WriteLine ("Main: I'm going away after waiting. My ID: " + Thread.CurrentThread.ManagedThreadId);        
}

We are using the overload – Task.Factory.StartNew(Action<object> action, object state). So we added a variable in the lambda and passed a value (1000) to the StartNew method.

Output:




I'm the main thread, starting a new task. My ID: 19
I'm a task running concurrently with the thread that started me. My ID: 4
Sleeping for 1000
Task: I'm going away after waking up. My ID: 4
Main: I'm going away after waiting. My ID: 19


Returning a value


This is a major difference between using ThreadPool’s QueueUserWorkItem and Task library. The QueueUserWorkItem gave back a bool value which was really of no use. But in TPL we have a Task object which represents a running operation, so we can use it to return a value as well.

We have to use the generic Task<T> when we want to create a task that returns a value. The type of return value is determined by the <T> parameter. Let’s see an example.

void Main()
{
    Console.WriteLine ("I'm the main thread, starting a new task. My ID: " + Thread.CurrentThread.ManagedThreadId);    
    //creating Task<int> which means a Task which returns an integer

    Task<int> t = Task.Factory.StartNew<int>((orderID) => 
    {
        Console.WriteLine ("Task: Calling service with input = " + orderID);
        int shippingRates = CalculateShippingRatesFromFedExWebService();
        Console.WriteLine ("Task: I'm going away. My ID: " + Thread.CurrentThread.ManagedThreadId);
        return shippingRates;        
    }, 980);
        
    int rates = t.Result;
    Console.WriteLine ("Main: Returned value from Task = " + rates);
    
    Console.WriteLine ("Main: I'm going away. My ID: " + Thread.CurrentThread.ManagedThreadId);
}

Changes:
- used the generic Task<int> in the task definition and StartNew method.
- added a return statement in the task body which returns an integer
- used Task.Result to get the return value

You may have noticed that we didn’t use “t.Wait” anymore to wait on the Task. This is because when the Result property is accessed then there is an implicit wait called on the task. If the task had been completed by the time we access the Result property, then it returns immediately otherwise it would wait until the task is finished.

Doing something after the task finishes

In the code samples above, we are running the task and then immediately waiting for it - which is not really useful. But if you think about UI applications (WPF or WinForms), then you do not need to wait on the task. The event handler or the UI command (in WPF) which starts the task can immediately return allowing the UI to be responsive and later when the task finishes it can go ahead and update the UI.

To specify a block of code that we want to run after a task finishes – or to say create a continuation task, we use “ContinueWith”.

void Main()
{
    //create task and start it immediately
    Task<int> t = Task.Factory.StartNew<int>(orderID =>
    {
        int shippingRates = CalculateShippingRatesFromFedEx(orderID);
        Console.WriteLine ("Task Thread ID:" + Thread.CurrentThread.ManagedThreadId);
        return shippingRates;
    }, 980);

    //attach the continuation block
    t.ContinueWith(antecedent =>
    {
        Console.WriteLine ("ContinueWith Thread ID:" + Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine ("Rates = " + antecedent.Result);       
    });   
    Console.WriteLine ("Main Ends");
}

Output:
Main Ends
Task Thread ID:12
ContinueWith Thread ID:12
Rates = 882

The ContinueWith method accepts an Action<Task> delegate. The parameter passed is of type Task and it references the task that ran before. Important concept is that ContinueWith returns a new Task. So using this we can create a chain of tasks each running one after another.

You may notice that both the task and its continuation task run on the same thread [ID:12]. But this is not always the case, the continuation task can be run on any thread. We can illustrate this by adding a Sleep in the Main method after creating the first task.

void Main()
{
    //create task and start it immediately
    Task<int> t = Task.Factory.StartNew<int>(orderID =>
    {
        int shippingRates = CalculateShippingRatesFromFedEx(orderID);
        Console.WriteLine ("Task Thread ID:" + Thread.CurrentThread.ManagedThreadId);
        return shippingRates;
    }, 980);

    Thread.Sleep(10);
    //attach the continuation block
    t.ContinueWith(antecedent =>
    {
        Console.WriteLine ("ContinueWith Thread ID:" + Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine ("Rates = " + antecedent.Result);       
    });   
    Console.WriteLine ("Main Ends");
}

Output:
Task Thread ID:15
Main Ends
ContinueWith Thread ID:12
Rates = 882


The only difference in previous two code snippets is the Thread.Sleep(10) introduced in the last one. Notice that the thread ID of task is now [15] and that of continuation is [12]. So it is not guaranteed, the TPL knows the best and decides which thread to use to run the continuation.

However, if you want to force that the continuation run on the same thread you can use the TaskContinuationOptions.ExecuteSynchronously when creating the continuation task. For e.g.:

//attach the continuation block
t.ContinueWith(antecedent =>
{
    Console.WriteLine ("ContinueWith Thread ID:" + Thread.CurrentThread.ManagedThreadId);
    Console.WriteLine ("Rates = " + antecedent.Result);       
},TaskContinuationOptions.ExecuteSynchronously);   


Assigning continuation task after task has completed

That is all good, but what if I create the continuation task after the antecedent task has already completed? Do I have to create the continuation task in the next line itself, after creating the antecedent task? Isn’t that brittle?

Good questions, let’s see what happens if we attach the continuation task after the task has finished.

void Main()
{
    Console.WriteLine ("Main Thread ID: " + Thread.CurrentThread.ManagedThreadId);
    //create task and start it immediately
    Task<int> t = Task.Factory.StartNew<int>(orderID =>
    {
        int shippingRates = CalculateShippingRatesFromFedEx(orderID);
        Console.WriteLine ("Task Thread ID: " + Thread.CurrentThread.ManagedThreadId);
        //Thread.Sleep(10);
        return shippingRates;
    }, 980);
    //main thread will block here for the task to finish
    Console.WriteLine ("1. Rates = " + t.Result);
    //get the task status
    Console.WriteLine ("Task Status = " + t.Status);
    //we attach the continuation block after the task is finished
    t.ContinueWith(antecedent =>
    {
        Console.WriteLine ("Continuation Task Thread ID: " + Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine ("2. Rates = " + antecedent.Result);       
    },TaskContinuationOptions.ExecuteSynchronously);   
    Console.WriteLine ("Main Ends");
}

Output:
Main Thread ID: 33
Task Thread ID: 12
1. Rates = 882
Task Status = RanToCompletion
Continuation Task Thread ID: 33
2. Rates = 882
Main Ends


In the code sample above, we attach a continuation task after the antecedent task “RanToCompletion”. Did you notice the thread ID of the continuation task in the output? It is the same as the Main thread. It means that a new Task wasn’t created to run the continuation task, the Main (or UI) thread ran it synchronously.

Coming soon...
- Updating UI from continuation task so that cross-thread access exceptions are not raised

- Exceptions in tasks






No comments:

Post a Comment

Note: only a member of this blog may post a comment.