Another feature of continuations is that you can continue continuations in order to chain tasks together to any length. The pipeline pattern can be implemented with a series of tasks and continuations. You can think of a pipeline as an assembly line in a factory. At the frontend of a pipeline, a producer task generates the data to be operated on, and each of the chained consumer stages operates on or changes the produced data.
In this recipe we will return to our word count example to create a simple three stage pipeline using continuations with TaskContinuationOptions.OnlyOnRanToCompletion
.
Open up Visual Studio, and let's see how to chain tasks together into a pipeline. The steps are as follows:
Continuation7
as the Solution name.using
directives to the top of your program class:using System; using System.Linq; using System.Net; using System.Threading.Tasks;
Main
method of the program class. In the catch
block add some handling for any AggregateException
raised by the tasks. At the end of the catch
block, write a message to the console to tell the user we are finished and wait for input to exit.try { //Task and continuations go here } catch (AggregateException aEx) { foreach (var ex in aEx.InnerExceptions) { Console.WriteLine("An exception has occured: {0}", ex.Message); } } Console.WriteLine(); Console.WriteLine("Complete. Please hit <Enter> to exit."); Console.ReadLine();
producer
task that reads in the text of a book, and returns a string array, which the consumer continuations will consume.var producer = Task.Factory.StartNew(() => { char[] delimiters = { ' ', ',', '.', ';', ':', '-', '_', '/', 'u000A' }; var client = new WebClient(); const string headerText = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)"; client.Headers.Add("user-agent", headerText); try { var words = client.DownloadString(@"http://www.gutenberg.org/files/2009/2009.txt"); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); Console.WriteLine("Word count for Origin of Species: {0}", wordArray.Count()); Console.WriteLine(); return wordArray; } finally { client.Dispose(); } });
Task<string[]> consumer1 = producer.ContinueWith(antecedent => { var wordsByUsage =antecedent.Result.Where(word => word.Length > 5) .GroupBy(word => word) .OrderByDescending(grouping => grouping.Count()) .Select(grouping => grouping.Key); var commonWords = (wordsByUsage.Take(5)).ToArray(); Console.WriteLine("The 5 most commonly used words in Origin of Species:"); Console.WriteLine("----------------------------------------------------"); foreach (var word in commonWords) { Console.WriteLine(word); } Console.WriteLine(); return antecedent.Result; }, TaskContinuationOptions.OnlyOnRanToCompletion); The second consumer will perform another Linq query to find the longest word used. Task consumer2 = consumer1.ContinueWith(antecedent => { var longestWord = (antecedent.Result.OrderByDescending(w => w.Length)).First(); Console.WriteLine("The longest word is: {0}", longestWord); }, TaskContinuationOptions.OnlyOnRanToCompletion); consumer2.Wait();
The task and continuations we used in this example are pretty much the same as the tasks we have created in other recipes. The primary difference is how we chained them together and the length of the chain. Our antecedent task produces and returns a string array, and then we have a continuation that finds the five most commonly used words, finally we continue the continuation to find the longest word.
Note that we also use TaskContinuationOptions.OnlyOnRanToCompletion
because we only want the consumers to be scheduled to run when the previous task succeeded. To be a more complete solution, we would want to use TaskContinuationOptions.OnlyOnFaulted
to set up a continuation for the failure path as well.
18.117.72.224