Home Random Page


CATEGORIES:

BiologyChemistryConstructionCultureEcologyEconomyElectronicsFinanceGeographyHistoryInformaticsLawMathematicsMechanicsMedicineOtherPedagogyPhilosophyPhysicsPolicyPsychologySociologySportTourism






Nbsp;   Cooperative Cancellation and Timeout

The Microsoft .NET Framework offers a standard pattern for canceling operations. This pattern is cooperative, meaning that the operation that you want to cancel has to explicitly support being can- celed. In other words, the code performing the operation that you want to cancel and the code that attempts to cancel the operation must both use the types mentioned in this section. It is nice when long-running compute-bound operations offer cancellation, so you should consider adding cancella- tion to your own compute-bound operations. In this section, I’ll explain how to accomplish this. But, first, let me explain the two main types provided in the Framework Class Library (FCL) that are part of the standard cooperative cancellation pattern.

To cancel an operation, you must first create a System.Threading.CancellationTokenSource

object. This class looks like this.

 

public sealed class CancellationTokenSource : IDisposable { // A reference type public CancellationTokenSource();

 

public Boolean IsCancellationRequested { get; } public CancellationToken Token { get; }

 

public void Cancel(); // Internally, calls Cancel passing false public void Cancel(Boolean throwOnFirstException);

...

}

 

This object contains all the states having to do with managing cancellation. After constructing a CancellationTokenSource (a reference type), one or more CancellationToken (a value type) instances can be obtained by querying its Token property and passed around to your operations that


allow themselves to be canceled. Here are the most useful members of the CancellationToken

value type.

 

public struct CancellationToken { // A value type

public static CancellationToken None { get; } // Very convenient

 

public Boolean IsCancellationRequested { get; } // Called by non­Task invoked operations public void ThrowIfCancellationRequested(); // Called by Task­invoked operations

 

// WaitHandle is signaled when the CancellationTokenSource is canceled public WaitHandle WaitHandle { get; }

// GetHashCode, Equals, operator== and operator!= members are not shown public Boolean CanBeCanceled { get; } // Rarely used

public CancellationTokenRegistration Register(Action<Object> callback, Object state, Boolean useSynchronizationContext); // Simpler overloads not shown

}

 

A CancellationToken instance is a lightweight value type because it contains a single private field: a reference to its CancellationTokenSource object. A compute-bound operation’s loop can periodically call CancellationToken’s IsCancellationRequested property to know if the loop should terminate early, thereby ending the compute-bound operation. Of course, the benefit here is that CPU time is no longer being wasted on an operation whose result you know you’re not interested in. Now, let me put all this together with some sample code.

 

internal static class CancellationDemo { public static void Main() {

CancellationTokenSource cts = new CancellationTokenSource();



 

// Pass the CancellationToken and the number­to­count­to into the operation ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000));

 

Console.WriteLine("Press <Enter> to cancel the operation."); Console.ReadLine();

cts.Cancel(); // If Count returned already, Cancel has no effect on it

// Cancel returns immediately, and the method continues running here...

 

Console.ReadLine();

}

 

private static void Count(CancellationToken token, Int32 countTo) { for (Int32 count = 0; count <countTo; count++) {

if (token.IsCancellationRequested) { Console.WriteLine("Count is cancelled"); break; // Exit the loop to stop the operation

}

 

Console.WriteLine(count);

Thread.Sleep(200); // For demo, waste some time

}

Console.WriteLine("Count is done");

}

}


 

If you’d like, you can call CancellationTokenSource’s Register method to register one or more methods to be invoked when a CancellationTokenSource is canceled. To this method, you pass an Action<Object> delegate, a state value that will be passed to the callback via the del- egate, and a Boolean indicating whether or not to invoke the delegate by using the calling thread’s SynchronizationContext. If you pass false for the useSynchronizationContext parameter, then the thread that calls Cancel will invoke all the registered methods sequentially. If you pass true for the useSynchronizationContext parameter, then the callbacks are sent (as opposed to posted) to the captured SynchronizationContext object that governs which thread invokes the callback. The SynchronizationContext class is discussed more in the “Applications and Their Threading Models” section in Chapter 28.

       
   
 
 

 

If Register is called multiple times, then multiple callback methods will be invoked. These callback methods could throw an unhandled exception. If you call CancellationTokenSource’s Cancel, passing it true, then the first callback method that throws an unhandled exception stops the other callback methods from executing, and the exception thrown will be thrown from Cancel as well. If you call Cancel passing it false, then all registered callback methods are invoked. Any unhandled exceptions that occur are added to a collection. After all callback methods have executed, if any of them threw an unhandled exception, then Cancel throws an AggregateException with its InnerExceptions property set to the collection of exception objects that were thrown. If no regis- tered callback methods threw an unhandled exception, then Cancel simply returns without throwing any exception at all.


 

CancellationToken’s Register method returns a CancellationTokenRegistration, which looks like this.

 

public struct CancellationTokenRegistration : IEquatable<CancellationTokenRegistration>, IDisposable { public void Dispose();

// GetHashCode, Equals, operator== and operator!= members are not shown

}

 

You can call Dispose to remove a registered callback from the CancellationTokenSource that it is associated with so that it does not get invoked when calling Cancel. Here is some code that demonstrates registering two callbacks with a single CancellationTokenSource.

 

var cts = new CancellationTokenSource(); cts.Token.Register(() => Console.WriteLine("Canceled 1")); cts.Token.Register(() => Console.WriteLine("Canceled 2"));

 

// To test, let's just cancel it now and have the 2 callbacks execute cts.Cancel();

 

When I run this code, I get the following output as soon as the Cancel method is called.

 

Canceled 2

Canceled 1

 

Finally, you can create a new CancellationTokenSource object by linking a bunch of other CancellationTokenSource objects. This new CancellationTokenSource object will be canceled when any of the linked CancellationTokenSource objects are canceled. The following code dem- onstrates.

 

// Create a CancellationTokenSource

var cts1 = new CancellationTokenSource(); cts1.Token.Register(() => Console.WriteLine("cts1 canceled"));

 

// Create another CancellationTokenSource var cts2 = new CancellationTokenSource();

cts2.Token.Register(() => Console.WriteLine("cts2 canceled"));

 

// Create a new CancellationTokenSource that is canceled when cts1 or ct2 is canceled var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token); linkedCts.Token.Register(() => Console.WriteLine("linkedCts canceled"));


// Cancel one of the CancellationTokenSource objects (I chose cts2) cts2.Cancel();

 

// Display which CancellationTokenSource objects are canceled Console.WriteLine("cts1 canceled={0}, cts2 canceled={1}, linkedCts canceled={2}",

cts1.IsCancellationRequested, cts2.IsCancellationRequested, linkedCts.IsCancellationRequested);

 

When I run the preceding code, I get the following output.

 

linkedCts canceled cts2 canceled

cts1 canceled=False, cts2 canceled=True, linkedCts canceled=True

 

It is often valuable to cancel an operation after a period of time has elapsed. For example, imagine a server application that starts computing some work based on a client request. But the server appli- cation needs to respond to the client within two seconds, no matter what. In some scenarios, it is bet- ter to respond in a short period of time with an error or with partially computed results as opposed to waiting a long time for a complete result. Fortunately, CancellationTokenSource gives you a way to have it self-cancel itself after a period of time. To take advantage of this, you can either construct a CancellationTokenSource object by using one of the constructors that accepts a delay, or you can call CancellationTokenSource’s CancelAfter method.

 

public sealed class CancellationTokenSource : IDisposable { // A reference type public CancellationTokenSource(Int32 millisecondsDelay);

public CancellationTokenSource(TimeSpan delay);

 


 

}

 

Tasks


public void CancelAfter(Int32 millisecondsDelay); public void CancelAfter(TimeSpan delay);

...


 

 

Calling ThreadPool’s QueueUserWorkItem method to initiate an asynchronous compute-bound operation is very simple. However, this technique has many limitations. The biggest problem is that there is no built-in way for you to know when the operation has completed, and there is no way to get a return value back when the operation completes. To address these limitations and more, Micro- soft introduced the concept of tasks, and you use them via types in the System.Threading.Tasks namespace.

So, instead of calling ThreadPool’s QueueUserWorkItem method, you can do the same via tasks.

 

ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5); // Calling QueueUserWorkItem

new Task(ComputeBoundOp, 5).Start(); // Equivalent of preceding using Task Task.Run(() => ComputeBoundOp(5)); // Another equivalent


In the second line of preceding code, I am creating the Task object and then immediately calling Start to schedule the task to run. Naturally, you can create the Task object and then call Start on it later. You could imagine code that creates a Task object and then passes it to some other method that decides when to call Start to schedule the task. Because it is common to create a Task object and then immediately call Start on it, you can call Task’s convenient static Run method as shown on the last line of the preceding code.

When creating a Task, you call a constructor, passing it an Action or an Action<Object> delegate that indicates the operation that you want performed. If you pass a method that expects an Object, then you must also pass to Task’s constructor the argument that you ultimately want passed to the operation. When calling Run, you can pass it an Action or Func<TResult> delegate indicating the operation you want performed. When calling a constructor or when calling Run, you

can optionally pass a CancellationToken, which allows the Task to be canceled before it has been

scheduled (see the “Canceling a Task” section later in this chapter).

 

You can also optionally pass to the constructor some TaskCreationOptions flags that control how the Task executes. TaskCreationOptions is an enumerated type defining a set of flags that you can bitwise-OR together. It is defined as follows.

 

[Flags, Serializable]

public enum TaskCreationOptions {

None = 0x0000,// The default

 

// Hints to the TaskScheduler that you want this task to run sooner than later. PreferFairness = 0x0001,

 

// Hints to the TaskScheduler that it should more aggressively create thread pool threads. LongRunning = 0x0002,

 

// Always honored: Associates a Task with its parent Task (discussed shortly) AttachedToParent = 0x0004,

 

// If a task attempts to attach to this parent task, it is a normal task, not a child task. DenyChildAttach = 0x0008,

 

// Forces child tasks to use the default scheduler as opposed to the parent’s scheduler. HideScheduler = 0x0010

}

 

Some of these flags are hints that may or may not be honored by the TaskScheduler that is being used to schedule a Task; the AttachedToParent, DenyChildAttach, and HideScheduler flags are always honored, because they have nothing to do with the TaskScheduler itself. Task­ Scheduler objects are discussed later in the “Task Schedulers” section.


Waiting for a Task to Complete and Getting Its Result

With tasks, it is also possible to wait for them to complete and then get their result. Let’s say that we have a Sum method that is computationally intensive if n is a large value.

 

private static Int32 Sum(Int32 n) { Int32 sum = 0;

for (; n > 0; n­­)

checked { sum += n; } // if n is large, this will throw System.OverflowException return sum;

}

 

We can now construct a Task<TResult> object (which is derived from Task), and we pass for the generic TResult argument the compute-bound operation’s return type. Now, after starting the task, we can wait for it to complete and then get its result by using the following code.

 

// Create a Task (it does not start running now)

Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 1000000000);

 

// You can start the task sometime later t.Start();

 

// Optionally, you can explicitly wait for the task to complete t.Wait(); // FYI: Overloads exist accepting timeout/CancellationToken

 

// You can get the result (the Result property internally calls Wait) Console.WriteLine("The Sum is: " + t.Result); // An Int32 value

       
   
 
 

 

If the compute-bound task throws an unhandled exception, the exception will be swallowed, stored in a collection, and the thread pool thread is allowed to return to the thread pool. When the Wait method or the Result property is invoked, these members will throw a System.Aggregate­ Exception object.


The AggregateException type is used to encapsulate a collection of exception objects (which can happen if a parent task spawns multiple child tasks that throw exceptions). It contains an Inner­ Exceptions property that returns a ReadOnlyCollection<Exception> object. Do not confuse the InnerExceptions property with the InnerException property, which the AggregateException class inherits from the System.Exception base class. For the preceding example, element 0 of AggregateException’s InnerExceptions property would refer to the actual System.Overflow­ Exception object thrown by the compute-bound method (Sum).

As a convenience, AggregateException overrides Exception’s GetBaseException method. AggregateException’s implementation returns the innermost AggregateException that is the root cause of the problem (assuming that there is just one innermost exception in the collection). AggregateException also offers a Flatten method that creates a new AggregateException, whose InnerExceptions property contains a list of exceptions produced by walking the original AggregateException’s inner exception hierarchy. Finally, AggregateException also provides a Handle method that invokes a callback method for each exception contained in the Aggregate­ Exception. The callback can then decide, for each exception, how to handle the exception; the callback returns true to consider the exception handled and false if not. If, after calling Handle, at least one exception is not handled, then a new AggregateException object is created contain- ing just the unhandled exceptions and the new AggregateException object is thrown. Later in this chapter, I show examples using the Flatten and Handle methods.

       
   
 
 

 

In addition to waiting for a single task, the Task class also offers two static methods that allow a thread to wait on an array of Task objects. Task’s static WaitAny method blocks the calling thread until any of the Task objects in the array have completed. This method returns an Int32 index into the array indicating which Task object completed, causing the thread to wake and continue run- ning. The method returns ­1 if the timeout occurs and throws an OperationCanceledException if WaitAny is canceled via a CancellationToken.

Similarly, the Task class has a static WaitAll method that blocks the calling thread until all the Task objects in the array have completed. The WaitAll method returns true if all the Task objects complete and false if a timeout occurs; an OperationCanceledException is thrown if WaitAll is canceled via a CancellationToken.


Canceling a Task

Of course, you can use a CancellationTokenSource to cancel a Task. First, we must revise our Sum

method so that it accepts a CancellationToken.

 

private static Int32 Sum(CancellationToken ct, Int32 n) { Int32 sum = 0;

for (; n > 0; n­­) {

 

// The following line throws OperationCanceledException when Cancel

// is called on the CancellationTokenSource referred to by the token ct.ThrowIfCancellationRequested();

 

checked { sum += n; } // if n is large, this will throw System.OverflowException

}

return sum;

}

 

In this code, the compute-bound operation’s loop periodically checks to see if the operation has been canceled by calling CancellationToken’s ThrowIfCancellationRequested method. This method is similar to CancellationToken’s IsCancellationRequested property shown earlier in the “Cooperative Cancellation and Timeout” section. However, ThrowIfCancellationRequested throws an OperationCanceledException if the CancellationTokenSource has been canceled. The reason for throwing an exception is because, unlike work items initiated with ThreadPool’s QueueUserWorkItem method, tasks have the notion of having completed and a task can even return a value. So, there needs to be a way to distinguish a completed task from a canceled task, and having the task throw an exception lets you know that the task did not run all the way to completion.

Now, we will create the CancellationTokenSource and Task objects as follows.

 

CancellationTokenSource cts = new CancellationTokenSource(); Task<Int32> t = Task.Run(() => Sum(cts.Token, 1000000000), cts.Token);

 

// Sometime later, cancel the CancellationTokenSource to cancel the Task cts.Cancel(); // This is an asynchronous request, the Task may have completed already

 

try {

// If the task got canceled, Result will throw an AggregateException Console.WriteLine("The sum is: " + t.Result); // An Int32 value

}

catch (AggregateException x) {

// Consider any OperationCanceledException objects as handled.

// Any other exceptions cause a new AggregateException containing

// only the unhandled exceptions to be thrown x.Handle(e => e is OperationCanceledException);

 

// If all the exceptions were handled, the following executes Console.WriteLine("Sum was canceled");

}


When creating a Task, you can associate a CancellationToken with it by passing it to Task’s constructor (as shown in the preceding code). If the CancellationToken gets canceled before the Task is scheduled, the Task gets canceled and never executes at all.2 But if the Task has already been scheduled (by calling the Start method), then the Task’s code must explicitly support cancel- lation if it allows its operation to be canceled while executing. Unfortunately, while a Task object has a CancellationToken associated with it, there is no way to access it, so you must somehow get the same CancellationToken that was used to create the Task object into the Task’s code itself. The easiest way to write this code is to use a lambda expression and “pass” the CancellationToken as a closure variable (as I’ve done in the previous code example).

 

 

Starting a New Task Automatically When Another Task Completes

In order to write scalable software, you must not have your threads block. This means that calling Wait or querying a task’s Result property when the task has not yet finished running will most likely cause the thread pool to create a new thread, which increases resource usage and hurts performance. Fortunately, there is a better way to find out when a task has completed running. When a task com- pletes, it can start another task. Here is a rewrite of the earlier code that doesn’t block any threads.

 

// Create and start a Task, continue with another task

Task<Int32> t = Task.Run(() => Sum(CancellationToken.None, 10000));

 

// ContinueWith returns a Task but you usually don't care

Task cwt = t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result));

 

Now, when the task executing Sum completes, this task will start another task (also on some thread pool thread) that displays the result. The thread that executes the preceding code does not block waiting for either of these two tasks to complete; the thread is allowed to execute other code or, if it is a thread pool thread itself, it can return to the pool to perform other operations. Note that the task executing Sum could complete before ContinueWith is called. This will not be a problem because the ContinueWith method will see that the Sum task is complete and it will immediately start the task that displays the result.

Also, note that ContinueWith returns a reference to a new Task object (which my code placed in the cwt variable). Of course, you can invoke various members (like Wait, Result, or even Continue­ With) using this Task object, but usually you will ignore this Task object and will not save a reference to it in a variable.

I should also mention that Task objects internally contain a collection of ContinueWith tasks. So you can actually call ContinueWith several times using a single Task object. When the task completes, all the ContinueWith tasks will be queued to the thread pool. In addition, when call- ing ContinueWith, you can specify a bitwise OR’d set of TaskContinuationOptions. The first six flags—None, PreferFairness, LongRunning, AttachedToParent, DenyChildAttach, and

 

 

 
 

2 By the way, if you try to cancel a task before it is even started, an InvalidOperationException is thrown.


HideScheduler—are identical to the flags offered by the TaskCreationOptions enumerated type shown earlier. Here is what the TaskContinuationOptions type looks like.

 

[Flags, Serializable]

public enum TaskContinuationOptions {

None = 0x0000,// The default

 

// Hints to the TaskScheduler that you want this task to run sooner than later. PreferFairness = 0x0001,

 

// Hints to the TaskScheduler that it should more aggressively create thread pool threads. LongRunning = 0x0002,

 

// Always honored: Associates a Task with its parent Task (discussed shortly) AttachedToParent = 0x0004,

 

// If a task attempts to attach to this parent task, an InvalidOperationException is thrown. DenyChildAttach = 0x0008,

 

// Forces child tasks to use the default scheduler as opposed to the parent’s scheduler. HideScheduler = 0x0010,

 

// Prevents completion of the continuation until the antecedent has completed. LazyCancellation = 0x0020,

 

// This flag indicates that you want the thread that executed the first task to also

// execute the ContinueWith task. If the first task has already completed, then the

// thread calling ContinueWith will execute the ContinueWith task. ExecuteSynchronously = 0x80000,

 

// These flags indicate under what circumstances to run the ContinueWith task NotOnRanToCompletion = 0x10000,

NotOnFaulted = 0x20000,

NotOnCanceled = 0x40000,

 

// These flags are convenient combinations of the above three flags OnlyOnCanceled = NotOnRanToCompletion | NotOnFaulted, OnlyOnFaulted = NotOnRanToCompletion | NotOnCanceled, OnlyOnRanToCompletion = NotOnFaulted | NotOnCanceled,

}

 

When you call ContinueWith, you can indicate that you want the new task to execute only if the first task is canceled by specifying the TaskContinuationOptions.OnlyOnCanceled flag. Similarly, you have the new task execute only if the first task throws an unhandled exception using the Task­ ContinuationOptions.OnlyOnFaulted flag. And, of course, you can use the TaskContinuation­ Options.OnlyOnRanToCompletion flag to have the new task execute only if the first task runs all the way to completion without being canceled or throwing an unhandled exception. By default, if you do not specify any of these flags, then the new task will run regardless of how the first task completes. When a Task completes, any of its continue-with tasks that do not run are automatically canceled.

Here is an example that puts all of this together.

 

// Create and start a Task, continue with multiple other tasks Task<Int32> t = Task.Run(() => Sum(10000));

// Each ContinueWith returns a Task but you usually don't care


t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result), TaskContinuationOptions.OnlyOnRanToCompletion);

 

t.ContinueWith(task => Console.WriteLine("Sum threw: " + task.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted);

 

t.ContinueWith(task => Console.WriteLine("Sum was canceled"), TaskContinuationOptions.OnlyOnCanceled);

 

 

A Task May Start Child Tasks

Finally, tasks support parent/child relationships, as demonstrated by the following code.

 

Task<Int32[]> parent = new Task<Int32[]>(() => {

var results = new Int32[3]; // Create an array for the results

 

// This tasks creates and starts 3 child tasks

new Task(() => results[0] = Sum(10000), TaskCreationOptions.AttachedToParent).Start(); new Task(() => results[1] = Sum(20000), TaskCreationOptions.AttachedToParent).Start(); new Task(() => results[2] = Sum(30000), TaskCreationOptions.AttachedToParent).Start();

 

// Returns a reference to the array (even though the elements may not be initialized yet) return results;

});

 

// When the parent and its children have run to completion, display the results var cwt = parent.ContinueWith(

parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));

 

// Start the parent Task so it can start its children parent.Start();

 

Here, the parent task creates and starts three Task objects. By default, Task objects created by another task are top-level tasks that have no relationship to the task that creates them. However, the TaskCreationOptions.AttachedToParent flag associates a Task with the Task that creates it so that the creating task is not considered finished until all its children (and grandchildren) have finished running. When creating a Task by calling the ContinueWith method, you can make the continue- with task be a child by specifying the TaskContinuationOptions.AttachedToParent flag.

 

Inside a Task

Each Task object has a set of fields that make up the task’s state. There is an Int32 ID (see Task’s read-only Id property), an Int32 representing the execution state of the Task, a reference to the parent task, a reference to the TaskScheduler specified when the Task was created, a reference to the callback method, a reference to the object that is to be passed to the callback method (queryable via Task’s read-only AsyncState property), a reference to an ExecutionContext, and a refer- ence to a ManualResetEventSlim object. In addition, each Task object has a reference to some supplementary state that is created on demand. The supplementary state includes a Cancellation­ Token, a collection of ContinueWithTask objects, a collection of Task objects for child tasks that have thrown unhandled exceptions, and more. My point is that although tasks provide you a lot of


features, there is some cost to tasks because memory must be allocated for all this state. If you don’t need the additional features offered by tasks, then your program will use resources more efficiently if you use ThreadPool.QueueUserWorkItem.

The Task and Task<TResult> classes implement the IDisposable interface, allowing you to call Dispose when you are done with the Task object. Today, all the Dispose method does is close the ManualResetEventSlim object. However, it is possible to define classes derived from Task and Task<TResult>, and these classes could allocate their own resources, which would be freed in their override of the Dispose method. I recommend that developers not explicitly call Dispose on a Task object in their code; instead, just let the garbage collector clean up any resources when it determines that they are no longer in use.

You’ll notice that each Task object contains an Int32 field representing a Task’s unique ID. When you create a Task object, the field is initialized to zero. Then the first time you query Task’s read-only Id property, the property assigns a unique Int32 value to this field and returns it from the property. Task IDs start at 1 and increment by 1 as each ID is assigned. Just looking at a Task object in the Microsoft Visual Studio debugger will cause the debugger to display the Task’s ID, forcing the Task to be assigned an ID.

The idea behind the ID is that each Task can be identified by a unique value. In fact, Visual Studio shows you these task IDs in its Parallel Tasks and Parallel Stacks windows. But because you don’t as- sign the IDs yourself in your code, it is practically impossible to correlate an ID number with what your code is doing. While running a task’s code, you can query Task’s static CurrentId property, which returns a nullable Int32 (Int32?). You can also call this from Visual Studio’s Watch window or Im- mediate window while debugging to get the ID for the code that you are currently stepping through. Then you can find your task in the Parallel Tasks/Stacks windows. If you query the CurrentId prop- erty while a task is not executing, it returns null.

During a Task object’s existence, you can learn where it is in its lifecycle by querying Task’s read- only Status property. This property returns a TaskStatus value that is defined as follows.

 

public enum TaskStatus {

// These flags indicate the state of a Task during its lifetime:

Created, // Task created explicitly; you can manually Start() this task WaitingForActivation,// Task created implicitly; it starts automatically

 

WaitingToRun, // The task was scheduled but isn’t running yet Running, // The task is actually running

 

// The task is waiting for children to complete before it considers itself complete WaitingForChildrenToComplete,

 

// A task's final state is one of these: RanToCompletion,

Canceled, Faulted

}

 

When you first construct a Task object, its status is Created. Later, when the task is started, its status changes to WaitingToRun. When the Task is actually running on a thread, its status changes


to Running. When the task stops running and is waiting for any child tasks, the status changes to WaitingForChildrenToComplete. When a task is completely finished, it enters one of three final states: RanToCompletion, Canceled, or Faulted. When a Task<TResult> runs to completion, you can query the task’s result via Task<TResult>’s Result property. When a Task or Task<TResult> faults, you can obtain the unhandled exception that the task threw by querying Task’s Exception property; which always returns an AggregateException object whose collection contains the set of unhandled exceptions.

For convenience, Task offers several read-only, Boolean properties: IsCanceled, IsFaulted, and IsCompleted. Note that IsCompleted returns true when the Task is in the RanToCompletion, Canceled, or Faulted state. The easiest way to determine if a Task completed successfully is to use code like the following.

 

if (task.Status == TaskStatus.RanToCompletion) ...

 

A Task object is in the WaitingForActivation state if that Task is created by calling one of these functions: ContinueWith, ContinueWhenAll, ContinueWhenAny, or FromAsync. A

Task created by constructing a TaskCompletionSource<TResult> object is also created in the WaitingForActivation state. This state means that the Task’s scheduling is controlled by the task infrastructure. For example, you cannot explicitly start a Task object that was created by calling ContinueWith. This Task will start automatically when its antecedent task has finished executing.

 

Task Factories

Occasionally, you might want to create a bunch of Task objects that share the same configuration. To keep you from having to pass the same parameters to each Task’s constructor over and over again, you can create a task factory that encapsulates the common configuration. The System.Threading. Tasks namespace defines a TaskFactory type as well as a TaskFactory<TResult> type. Both of these types are derived from System.Object; that is, they are peers of each other.

If you want to create a bunch of tasks that return void, then you will construct a TaskFactory.

If you want to create a bunch of tasks that have a specific return type, then you will construct a TaskFactory<TResult> where you pass the task’s desired return type for the generic TResult argument. When you create one of these task factory classes, you pass to its constructor the defaults that you want the tasks that the factory creates to have. Specifically, you pass to the task factory the CancellationToken, TaskScheduler, TaskCreationOptions, and TaskContinuationOptions settings that you want factory-created tasks to have.

Here is some sample code demonstrating the use of a TaskFactory.

 

Task parent = new Task(() => {

var cts = new CancellationTokenSource();

var tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);

 

// This task creates and starts 3 child tasks var childTasks = new[] {

tf.StartNew(() => Sum(cts.Token, 10000)),


tf.StartNew(() => Sum(cts.Token, 20000)),

tf.StartNew(() => Sum(cts.Token, Int32.MaxValue)) // Too big, throws OverflowException

};

 

// If any of the child tasks throw, cancel the rest of them for (Int32 task = 0; task < childTasks.Length; task++)

childTasks[task].ContinueWith(

t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted);

 

// When all children are done, get the maximum value returned from the

// non­faulting/canceled tasks. Then pass the maximum value to another

// task that displays the maximum result tf.ContinueWhenAll(

childTasks, completedTasks =>

completedTasks.Where(t => t.Status == TaskStatus.RanToCompletion).Max(t => t.Result), CancellationToken.None)

.ContinueWith(t =>Console.WriteLine("The maximum is: " + t.Result), TaskContinuationOptions.ExecuteSynchronously);

});

 

// When the children are done, show any unhandled exceptions too parent.ContinueWith(p => {

// I put all this text in a StringBuilder and call Console.WriteLine just once

// because this task could execute concurrently with the task above & I don't

// want the tasks' output interspersed StringBuilder sb = new StringBuilder(

"The following exception(s) occurred:" + Environment.NewLine);

 

foreach (var e in p.Exception.Flatten().InnerExceptions) sb.AppendLine(" "+ e.GetType().ToString());

Console.WriteLine(sb.ToString());

}, TaskContinuationOptions.OnlyOnFaulted);

 

 

// Start the parent Task so it can start its children parent.Start();

 

With this code, I am creating a TaskFactory<Int32> object that I will use to create three Task objects. I want to configure the child tasks all the same way: each Task object shares the same CancellationTokenSource token, tasks are considered children of their parent, all continue-with tasks created by the TaskFactory execute synchronously, and all Task objects created by this TaskFactory use the default TaskScheduler.

Then I create an array consisting of the three child Task objects, all created by calling Task­ Factory’s StartNew method. This method conveniently creates and starts each child task. In a loop, I tell each child task that throws an unhandled exception to cancel all the other child tasks that are still running. Finally, using the TaskFactory, I call ContinueWhenAll, which creates a Task that runs when all the child tasks have completed running. Because this task is created with the Task­ Factory, it will also be considered a child of the parent task and it will execute synchronously using the default TaskScheduler. However, I want this task to run even if the other child tasks were can- celed, so I override the TaskFactory’s CancellationToken by passing in CancellationToken. None, which prevents this task from being cancelable at all. Finally, when the task that processes all


the results is complete, I create another task that displays the highest value returned from all the child tasks.

       
   
 
 

 

 

Task Schedulers

The task infrastructure is very flexible, and TaskScheduler objects are a big part of this flexibility. A TaskScheduler object is responsible for executing scheduled tasks and also exposes task informa- tion to the Visual Studio debugger. The FCL ships with two TaskScheduler-derived types: the thread pool task scheduler and a synchronization context task scheduler. By default, all applications use the thread pool task scheduler. This task scheduler schedules tasks to the thread pool’s worker threads and is discussed in more detail in this chapter’s “How the Thread Pool Manages Its Threads” section. You can get a reference to the default task scheduler by querying TaskScheduler’s static Default property.

The synchronization context task scheduler is typically used for applications sporting a graphi- cal user interface, such as Windows Forms, Windows Presentation Foundation (WPF), Silverlight, and Windows Store applications. This task scheduler schedules all tasks onto the application’s GUI thread so that all the task code can successfully update UI components like buttons, menu items, and so on. The synchronization context task scheduler does not use the thread pool at all. You can get a refer-

ence to a synchronization context task scheduler by querying TaskScheduler’s static FromCurrent­ SynchronizationContext method.

Here is a simple Windows Forms application that demonstrates the use of the synchronization context task scheduler.

 

internal sealed class MyForm : Form {

private readonly TaskScheduler m_syncContextTaskScheduler; public MyForm() {

// Get a reference to a synchronization context task scheduler m_syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

 

Text = "Synchronization Context Task Scheduler Demo"; Visible = true; Width = 600; Height = 100;

}

 

private CancellationTokenSource m_cts;

 

protected override void OnMouseClick(MouseEventArgs e) {

if (m_cts != null) { // An operation is in flight, cancel it m_cts.Cancel();


m_cts = null;

} else { // An operation is not in flight, start it Text = "Operation running";

m_cts = new CancellationTokenSource();

 

// This task uses the default task scheduler and executes on a thread pool thread Task<Int32> t = Task.Run(() => Sum(m_cts.Token, 20000), m_cts.Token);

 

// These tasks use the sync context task scheduler and execute on the GUI thread t.ContinueWith(task => Text = "Result: " + task.Result,

CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, m_syncContextTaskScheduler);

 

t.ContinueWith(task => Text = "Operation canceled", CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled, m_syncContextTaskScheduler);

 

t.ContinueWith(task => Text = "Operation faulted", CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, m_syncContextTaskScheduler);

}

base.OnMouseClick(e);

}

}

 

When you click in the client area of this form, a compute-bound task will start executing on a thread pool thread. This is good because the GUI thread is not blocked during this time and can therefore respond to other UI operations. However, the code executed by the thread pool thread should not attempt to update UI components or else an InvalidOperationException will be thrown.

When the compute-bound task is done, one of the three continue-with tasks will execute. These tasks are all issued against the synchronization context task scheduler corresponding to the GUI thread, and this task scheduler queues the tasks to the GUI thread, allowing the code executed by these tasks to update UI components successfully. All of these tasks update the form’s caption via the inherited Text property.

Because the compute-bound work (Sum) is running on a thread pool thread, the user can interact with the UI to cancel the operation. In my simple code example, I allow the user to cancel the opera- tion by clicking in the form’s client area while an operation is running.

You can, of course, define your own class derived from TaskScheduler if you have special task scheduling needs. Microsoft has provided a bunch of sample code for tasks and includes the source code for a bunch of task schedulers in the Parallel Extensions Extras package, which can be down- loaded from here: http://code.msdn.microsoft.com/ParExtSamples. Here are some of the task schedul- ers included in this package:

IOTaskSchedulerThis task scheduler queues tasks to the thread pool’s I/O threads instead of its worker threads.

LimitedConcurrencyLevelTaskSchedulerThis task scheduler allows no more than n (a constructor parameter) tasks to execute simultaneously.


OrderedTaskSchedulerThis task scheduler allows only one task to execute at a time. This class is derived from LimitedConcurrencyLevelTaskScheduler and just passes 1 for n.

PrioritizingTaskSchedulerThis task scheduler queues tasks to the CLR’s thread pool. After this has occurred, you can call Prioritize to indicate that a Task should be processed be- fore all normal tasks (if it hasn’t been processed already). You can call Deprioritize to make a Task be processed after all normal tasks.

ThreadPerTaskSchedulerThis task scheduler creates and starts a separate thread for each task; it does not use the thread pool at all.

 

 


Date: 2016-03-03; view: 757


<== previous page | next page ==>
Nbsp;   Execution Contexts | Nbsp;   Parallel’s Static For, ForEach, and Invoke Methods
doclecture.net - lectures - 2014-2024 year. Copyright infringement or personal data (0.045 sec.)