If you can use async / wait, Brandon has a good answer. If you're still on VS2010, the first thing I would do to clear the serial version is to get an extension method similar to Then method by Stephen Tuub described in a blog post . I would also implement the Task.FromResult method if you are not using .NET 4.5. With them you can get:
public Task<string> DoWorkInSequence() { return Task.FromResult(4) .Then(x => { if (x != 5) { return Task.FromResult(true) .Then(y => { if (y) { return Task.FromResult(x.ToString() + y.ToString()); } else { return Task.FromResult("Nothing"); } }); } else { return Task.FromResult("Nothing"); } }); }
In addition, you usually have to return the task instead of the TaskCompletionSource object (which you can get by calling .Task on the TaskCompletionSource), since you do not want the caller to call the result for the task you are returning to them.
Brandon's answer also provides a good way to implement timeout functionality (correcting the lack of async / await keywords).
EDIT To reduce arrow code, we can implement more LINQ methods. The implementation of SelectMany is provided in a previously related blog post. Other methods we will need for LINQ are Select and Where. They should be simple enough once you have done Then and SelectMany, but here they are:
public static Task<T> Where<T>(this Task<T> task, Func<T, bool> predicate) { if (task == null) throw new ArgumentNullException("task"); if (predicate == null) throw new ArgumentNullException("predicate"); var tcs = new TaskCompletionSource<T>(); task.ContinueWith((completed) => { if (completed.IsFaulted) tcs.TrySetException(completed.Exception.InnerExceptions); else if (completed.IsCanceled) tcs.TrySetCanceled(); else { try { if (predicate(completed.Result)) tcs.TrySetResult(completed.Result); else tcs.TrySetCanceled(); } catch (Exception ex) { tcs.TrySetException(ex); } } }); return tcs.Task; } public static Task<TResult> Select<T, TResult>(this Task<T> task, Func<T, TResult> selector) { if (task == null) throw new ArgumentNullException("task"); if (selector == null) throw new ArgumentNullException("selector"); var tcs = new TaskCompletionSource<TResult>(); task.ContinueWith((completed) => { if (completed.IsFaulted) tcs.TrySetException(completed.Exception.InnerExceptions); else if (completed.IsCanceled) tcs.TrySetCanceled(); else { try { tcs.TrySetResult(selector(completed.Result)); } catch (Exception ex) { tcs.TrySetException(ex); } } }); return tcs.Task; }
After that, one final non-LINQ extension method allows you to use the default values for cancellation to return:
public static Task<T> IfCanceled<T>(this Task<T> task, T defaultValue) { if (task == null) throw new ArgumentNullException("task"); var tcs = new TaskCompletionSource<T>(); task.ContinueWith((completed) => { if (completed.IsFaulted) tcs.TrySetException(completed.Exception.InnerExceptions); else if (completed.IsCanceled) tcs.TrySetResult(defaultValue); else tcs.TrySetResult(completed.Result); }); return tcs.Task; }
And the new and improved DoWork (sans timeout):
public static Task<string> DoWorkInSequence() { return (from x in Task_FromResult(5) where x != 5 from y in Task_FromResult(true) where y select x.ToString() + y.ToString() ).IfCanceled("Nothing"); }
The Timeout method from Brandon's response (after overwriting, if necessary without async / await) may get stuck at the end of the chain for a common timeout and / or after each step in the chain if you want to continue further steps as soon as a common timeout is reached. Another possibility to break the chain would be to take all the individual steps by taking a cancellation token and modify the Timeout method to take the CancellationTokenSource and cancel it if a timeout occurs, and also throw a timeout exception.
EDIT (Brent Arias)
Taking fantastic ideas from what you presented, I developed what, in my opinion, is the final answer from my POV. It is based on the .net 4.0 extension methods found in the nuget ParallelExtensionsExtras package. In the example below, a third task is added to help illustrate the “feel” of programming for sequential tasks, given my stated requirements:
public Task<string> DoWorkInSequence() { var cts = new CancellationTokenSource(); Task timer = Task.Factory.StartNewDelayed(200, () => { cts.Cancel(); }); Task<int> AlphaTask = Task.Factory .StartNew(() => 4 ) .Where(x => x != 5 && !cts.IsCancellationRequested); Task<bool> BravoTask = AlphaTask .Then(x => true) .Where(x => x && !cts.IsCancellationRequested); Task<int> DeltaTask = BravoTask .Then(x => 7) .Where(x => x != 8); Task<string> final = Task.Factory .WhenAny(DeltaTask, timer) .ContinueWith(x => !DeltaTask.IsCanceled && DeltaTask.Status == TaskStatus.RanToCompletion ? AlphaTask.Result.ToString() + BravoTask.Result.ToString() + DeltaTask.Result.ToString(): "Nothing");
There are a few key points I made in this discussion and collaborative effort:
- Using the "Then" extension is good in trivial cases, but has noticeable limited applicability. For more complex cases, it is necessary to replace it, for example
.ContinueWith(x => true, cts.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default) . When replacing "Then" with "ContinueWith" for my declared scenario, it is important to add the OnlyOnRanToCompletion parameter. - Using the Timeout extension ultimately just doesn't work in my scenario. This is because it will only result in the cancellation of the Task to which it is attached directly, instead of canceling all instances of the Antecedant instance in the sequence. That's why I switched to
StartNewDelayed(...) tactics and added express cancellation in every Where clause. - Despite the fact that the ParallelExtensionsExtras library has LINQ to Tasks , which was defined by you, I came to the conclusion that it is better to avoid LINQ-ish with tasks. This is because tasks with LINQ are very esoteric ; this most likely confuses the hell out of the average developer. It's hard enough to make them understand asynchronous coding. Even the author of LINQ Tasks said, " How useful this LINQ implementation is in practice is controversial , but at least it provides interesting thought exercises." Yes, I agree, an interesting thought. Of course, I should at least admit the LINQ to Tasks “Where” method, since it played a key role in the solution that I listed above.