Process Tasks by Completion Idiom

Process Tasks by Completion Idiom

When working with tasks, there is often a problem: we have a set of input data and for processing each element we use a long-running operation.
When working with tasks, there is often a problem: we have a set of input data and for processing each element we use a long-running operation.

You can approach this task in a straightforward way: run the loop, launch the tasks, and process results one by one:

Completion_Idiom_1.jpg


Here we address a weather service in order to obtain the temperature in each city, and then process the obtained results by displaying the city and the temperature on the screen.

The approach is working, but there is one problem: the new task will be launched only after the completion of the previous one. You can force all tasks and call ToList() via LINQ request, but in this case the tasks will be processed in the order of the "cities" and not in the order of the results’ availability (let’s assume that obtaining the weather data for the first city will take three times longer than for other cities, in which case we will be waiting for the results from the first city, although the results from the other two are already available).

The solution is to use the Process Tasks by Completion idiom (you may call it a pattern, if you want), which is as follows: the tasks should be handled not in the order of their launch, but in the order of their completion.

Here's how it will look like:

Completion_Idiom_2.jpg


NOTE

In one of the Pluralsight courses this idiom is not called ‘Process Tasks One by One’ but IMHO - this name is even less clear than mine. So if you have any thoughts on a name which is better for conveying its essence, I will be glad to hear them!

The method ManualProcessByCompletion starts getting weather data via GetWeatherForAsync for all cities, wrapping an object of an anonymous type into a task (later I will explain why this is necessary). Then, inside the while loop we call Task.WhenAny and receive the first completed task. It turns out that the tasks are processed in the order of completion, not in the order of launching. But in this case, the task contains the answer to the question (what is the weather like in the city?), but does not include the question itself (the name of the city). We need to somehow combine the results and the execution context. To do this, we use the method TaskEx.FromTask:

Completion_Idiom_3.jpg


The TaskEx.FromResult method creates a proxy task which will end at the completion of the original task. A taskSelector delegate allows you to "extract" the task from the main object; this allows you to conveniently use this approach together with anonymous types.

The new approach works better than the original one, but it is more cumbersome. It makes sense to make a little wrapper which allows you to reuse it.

Completion_Idiom_4.jpg


The complete class code is available on GitHub, but here’s what it means. The method converts one task sequence to another task sequence, the order of which is determined by the order of task completion, not by the order of the original sequence.

And here's how an example looks like:

Completion_Idiom_5.jpg


We are back to the original version in terms of syntax, but there is some new behavior. Here is the result of the execution which shows that all the tasks are launched simultaneously, and the processing takes place as the results become available:

[12:54:35 PM]: Getting the weather for 'Moscow'
[12:54:35 PM]: Getting the weather for 'Seattle'
[12:54:35 PM]: Getting the weather for 'New York'
[12:54:36 PM]: Processing weather for 'Seattle': 'Temp: 7C'
Got the weather for 'Moscow'
[12:54:39 PM]: Processing weather for 'Moscow': 'Temp: 6C'
Got the weather for 'New York'
[12:54:40 PM]: Processing weather for 'New York': 'Temp: 8C'

UPDATE:

Removal of Query Comprehension Syntax will allow you to simplify the last example a little more:

Completion_Idiom_6.jpg


And the option proposed by @hazzik which is based on Rx-es:

Completion_Idiom_7.jpg


It works as follows: first we take a sequence of tasks, convert it into a sequence of IEnumerable>, which is then merged into one sequence IObservable.

The feature of this approach is that in this case the call of MoveNext in a foreach loop is blocking.

P.S. The code is uploaded to a new repo on GitHub - https://github.com/SergeyTeplyakov/TplTipsAndTricks


Sergey Teplyakov
Expert in .Net, С++ and Application Architecture
Nadal masz pytania?
Połącz sięz nami