Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the proper way to chain Tasks when returning a Task?

I am so so with using Tasks in C# but I get confused when I try to return a Task from a method and that method will do multiple tasks within itself. So do I have my method spin up a new Task and then do everything sequentially inside of there? It's hard to wrap my head around doing it all with .ContinueWith()

Example:

public Task<string> GetSomeData(CancellationToken token)
{
    return Task.Factory.StartNew(() =>
    {
        token.ThrowIfCancellationRequested();

        var initialData = GetSomeInteger(token).Result;

        return GetSomeString(initialData, token).Result;
    });
}

public Task<int> GetSomeInteger(CancellationToken token)
{
    return Task<int>.Factory.StartNew(() =>
    {
        return 4;
    }, token);
}

public Task<string> GetSomeString(int value, CancellationToken token)
{
    return Task<string>.Factory.StartNew(() =>
    {
        return value.ToString();
    }, token);
}

I am unsure how else to write this method to make it use Tasks correctly. I guess I just feel like there should be a .ContinueWith in there or something.

Possible fix??

public Task<string> GetSomeData(CancellationToken token)
{
    return GetSomeInteger(token).ContinueWith((prevTask) =>
    {
        return GetSomeString(prevTask.Result, token);
    }, token).Unwrap();
}
like image 461
Travyguy9 Avatar asked Aug 02 '12 18:08

Travyguy9


2 Answers

In general, it's often best to try to avoid spinning up new tasks if you are already working with task-based methods. Chaining tasks instead of blocking explicitly will reduce the overhead of the system, as it won't keep a ThreadPool thread tied up waiting.

That being said, it's often simpler to just block as you're doing.

Note that C# 5 makes this far simpler, providing an API that gives you the best of both:

public async Task<string> GetSomeData(CancellationToken token)
{
    token.ThrowIfCancellationRequested();

    var initialData = await SomeOtherMethodWhichReturnsTask(token);

    string result = await initialData.MethodWhichAlsoReturnsTask(token);

    return result;
};

Edit after update:

Given the new code, there isn't an easy way to chain this directly with ContinueWith. There are a couple of options. You can use Unwrap to convert the Task<Task<string>> you'd create, ie:

public Task<string> GetSomeData(CancellationToken token)
{
    Task<Task<string>> task = GetSomeInteger(token)
                               .ContinueWith(t => 
                               {
                                   return GetSomeString(t.Result, token);
                               }, token);
    return task.Unwrap();
}

Alternatively, you can handle the unwrapping yourself elegantly with TaskCompletionSource<T>:

public Task<string> GetSomeData(CancellationToken token)
{
    var tcs = new TaskCompletionSource<string>();

    Task<int> task1 = GetSomeInteger(token);
    Task<Task<string>> task2 = task1.ContinueWith(t => GetSomeString(t.Result, token));
    task2.ContinueWith(t => tcs.SetResult(t.Result.Result));
    return tcs.Task;
}

This allows the entire process to work without creating a new Task (which ties up a threadpool thread), and without ever blocking.

Note that you would probably want to add continuations on cancellation, and use tcs.SetCancelled when a cancellation was requested, as well.

like image 151
Reed Copsey Avatar answered Nov 08 '22 06:11

Reed Copsey


Here is an extension method that I built to solve this. Works in .Net 4+

public static Task<TNewResult> ContinueWith<T, TNewResult>(this Task<T> task, Func<Task<T>, Task<TNewResult>> continuationFunction, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<TNewResult>();
    task.ContinueWith(t => 
    {
        if (cancellationToken.IsCancellationRequested)
        {
            tcs.SetCanceled();
        }
        continuationFunction(t).ContinueWith(t2 => 
        {
            if (cancellationToken.IsCancellationRequested || t2.IsCanceled)
            {
                tcs.TrySetCanceled();
            }
            else if (t2.IsFaulted)
            {
                tcs.TrySetException(t2.Exception);
            }
            else
            {
                tcs.TrySetResult(t2.Result);
            }
        });
    });
    return tcs.Task;
}
like image 6
Kraig McConaghy Avatar answered Nov 08 '22 07:11

Kraig McConaghy