7

I have 3 tasks in my application that are responsible for getting data from databases.
Till now I had all tasks executed one after one. If first finished and had Result then this was my data, if now I started second task and I checked again.

Recently I found information that I can start multiple tasks and continue when one of them finish using Task.Factory.ContinueWhenAny. This works fine if all my task don't throw any Exceptions, but if any Task fails I can't get results I want.

For example:

var t1 = Task.Factory.StartNew(() =>
{
    Thread.Sleep(5000);
    return 1;
});

var t2 = Task.Factory.StartNew(() =>
{
    Thread.Sleep(2000);
    throw new Exception("My error");
    return 2;
});

var t3 = Task.Factory.StartNew(() =>
{
    Thread.Sleep(4000);
    return 3;
});

Task.Factory.ContinueWhenAny(new[] {t1, t2,t3}, (t) =>
{
    Console.WriteLine(t.Result);
});

This code starts 3 tasks and waits till one of them finish. Because t2 throws exception after 2 seconds it is the one that is available in ContinueWhenAny.

From code above I would like to get 3 in t.Result.
Is there an option to continue only when task finished successfully? Something like Task.Factory.ContinueWhenAnyButSkipFailedTasks

EDIT 1 This is my solution for now based on @Nitram answer:

var t1 = Task.Factory.StartNew(() =>
{
    var rnd = new Random();
    Thread.Sleep(rnd.Next(5,15)*1000);
    throw new Exception("My error");
    return 1;
});

var t2 = Task.Factory.StartNew(() =>
{
    Thread.Sleep(2000);
    throw new Exception("My error");
    return 2;
});

var t3 = Task.Factory.StartNew(() =>
{
    throw new Exception("My error");
    return 3;
});

var tasks = new List<Task<int>> { t1, t2, t3 };

Action<Task<int>> handler = null;

handler = t =>
{
    if (t.IsFaulted)
    {
        tasks.Remove(t);
        if (tasks.Count == 0)
        {
            throw new Exception("No data at all!");
        }
        Task.Factory.ContinueWhenAny(tasks.ToArray(), handler);
    }
    else
    {
        Console.WriteLine(t.Result);
    }
};

Task.Factory.ContinueWhenAny(tasks.ToArray(), handler);

What I need now is how to throw exception when all tasks throw exception?
Maybe this can be changed into single method that would return task - something like child tasks?

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
Misiu
  • 4,738
  • 21
  • 94
  • 198
  • I think you need to write your own Task.WhenAny version that takes a Func that specifies whether this particular task completion is supposed to complete the WhenAny. – usr Jun 25 '15 at 11:22

3 Answers3

3

There is a overload to the ContinueWhenAny function that does what you want.

Simply set the TaskContinuationOptions to OnlyOnRanToCompletion and the failed tasks will be ignored.

Task.Factory.ContinueWhenAny(new[] {t1, t2,t3}, (t) =>
{
    Console.WriteLine(t.Result);
}, TaskContinuationOptions.OnlyOnRanToCompletion);

So we concluded that this answer is actually wrong.

The removal of the tasks from a list seems to be the only way I can think of. I tried to put this into some lines of code. Here you go:

var tasks = new List<Task> {t1, t2, t3};

Action<Task> handler = null;
handler = (Task t) =>
{
    if (t.IsFauled) {
        tasks.Remove(t);
        Task.Factory.ContinueWhenAny(tasks.ToArray, handler);
    } else {
        Console.WriteLine(t.Result);
    }
};
Task.Factory.ContinueWhenAny(tasks.ToArray, handler);

I am not very firm with C#, but I hope it gives you a idea. What is basically happening is that every time a task that is faulted is handled, this task is removed from the list of known tasks and the function waits for the next one.

Okay and now the entire thing with .NET 4.5 and the async-await pattern. await basically gives you the ability to register what ever is written after the await into a continuation.

So this is nearly the same pattern with async-await.

var tasks = new List<Task> {t1, t2, t3};
while (tasks.Any()) 
{
    var finishedTask = await Task.WhenAny(tasks);
    if (finishedTask.IsFaulted)
    {
        tasks.Remove(finishedTask);
    }
    else
    {
        var result = await finishedTask;
        Console.WriteLine(result);
        return;
    }
}

The only difference is that the outer function needs to be a async function for that one. This means upon encountering the first await the outer function will return the Task that holds the continuation.

You can add a surrounding function that blocks until this function is done. The async-await pattern gives you the ability to write asynchronous non-blocking code that "looks" like simply synchronouse code.

Also I suggest you use the Task.Run function to spawn your tasks instead of the TaskFactory. It will spare from some problems later on. ;-)

Misiu
  • 4,738
  • 21
  • 94
  • 198
Nitram
  • 6,486
  • 2
  • 21
  • 32
  • Thank You for such fast answer, but unfortunately I get error `It is invalid to exclude specific continuation kinds for continuations off of multiple tasks.` – Misiu Jun 25 '15 at 10:55
  • Ah... that happens if you don't read the documentation properly. Sorry this answer is wrong. Because the NotOn* and OnlyOn* options are illegal for this function. As it is the only idea I got is to maintain a list of the tasks you wait on and if the function triggers on a failed task you could remove this one and wait for the remaining. – Nitram Jun 25 '15 at 10:58
  • Could You show some sample code? It would be very helpful. – Misiu Jun 25 '15 at 11:10
  • Is there any explicit reason you want to use this schema? Or do you consider using the async-await pattern? – Nitram Jun 25 '15 at 11:18
  • @Misiu I added some code to the answer that may give you a idea how to solve it. I wrote this in 2 minutes and I am not very good with C#. So please execute any mistakes there. I am more of a VB person. – Nitram Jun 25 '15 at 11:27
  • The new code is racy because multiple handlers can complete at the same time. Also it has quadratic performance. – usr Jun 25 '15 at 11:32
  • @usr You sure? I don't think multiple handlers are active at the same time. Because `ContinueWhenAny` only fires once, as soon as it encounters an task that finished and only for one task. So the a single `ContinueWhenAny` shouldn't result in more than one call of the `continuationFunction` – Nitram Jun 25 '15 at 11:35
  • @Nitram I don't have any reasons behind this schema, I could use async await, but because I'm starting with those things I don't know how to use them. – Misiu Jun 25 '15 at 11:37
  • @Misiu do you use .NET 4.5 or .NET 4.0 ? – Nitram Jun 25 '15 at 11:39
  • @Misiu Now this answer is slowly getting a bit long. I edited it to provide a async-await example. I highly recomment you also search the internet for some more examples how to use this. If you are using the latest .NET frameworks It is really a good idea to learn how to use this. It is much easier than to use the Task Continuations directly. – Nitram Jun 25 '15 at 11:59
  • `If you are using the latest .NET frameworks It is really a good idea to learn how to use this` that is so correct. I think I must spend some time more on this async await thing. I'll check Your code right away, but it should work without any problems. – Misiu Jun 25 '15 at 12:27
  • @Misiu Don't forget to mark one of the answers as correct. – Nitram Jun 25 '15 at 13:36
3

If you're using .NET 4.5, you can use Task.WhenAny to easily achieve what you want:

public async Task<int> GetFirstCompletedTaskAsync()
{
    var tasks = new List<Task> 
    {
        Task.Run(() =>
        {
            Thread.Sleep(5000);
            return 1;
        }),
        Task.Run(() =>
        {
            Thread.Sleep(2000);
            throw new Exception("My error");
        }),
        Task.Run(() =>
        {
            Thread.Sleep(4000);
            return 3;
        }),
    };

    while (tasks.Count > 0)
    {
        var finishedTask = await Task.WhenAny(tasks);
        if (finishedTask.Status == TaskStatus.RanToCompletion)
        {
            return finishedTask
        }

        tasks.Remove(finishedTask);
    }
    throw new WhateverException("No completed tasks");
}
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • I need slightly different version - I need to get first result from all tasks, but only from that task that completed. If all of them are faulted I want to throw exception. – Misiu Jun 25 '15 at 12:26
  • This one has quadratic runtime in the number of tasks as well. Also quadratic memory consumption for the continuations. – usr Jun 25 '15 at 13:22
  • @usr Do explain why this has quadric runtime and memory consumption? – Yuval Itzchakov Jun 25 '15 at 13:47
  • @YuvalItzchakov He tried to blame that on my answer too. ;-) – Nitram Jun 25 '15 at 14:08
  • 1
    WhenAny hooks up continuations on all N tasks and the loop runs N times. In the other answer there were N Remove operations each of which take N/2 operations. – usr Jun 25 '15 at 14:15
  • Is there a way to adopt this code for ASP.NET MVC? It gives random exception `[InvalidOperationException: An asynchronous module or handler completed while an asynchronous operation was still pending.]` when you try to execute it while some previous tasks are not finished. – Anton Papin Jun 15 '17 at 10:27
  • @AntonPapin The problem with MVC is you must wait for all tasks completion prior to the controller ending the session and returning the response. If you want to ignore that (which I don't recommend you do), you can use `Task.Factory.StartNew` instead of `Task.Run` which doesn't register continuations with the ASP.NET runtime. For more on that see https://stackoverflow.com/questions/28805796/asp-net-controller-an-asynchronous-module-or-handler-completed-while-an-asynchr – Yuval Itzchakov Jun 15 '17 at 10:29
2

What if just simply do this (at least it worked for me):

        bool taskFinishedFlag = false;

        Task t1 = Task.Factory.StartNew(() => { Thread.Sleep(4000); return 1; });

        Task t2 = Task.Factory.StartNew(() => { Thread.Sleep(2000); 
                                                throw new Exception("");return 2; });

        Task t3 = Task.Factory.StartNew(() => { Thread.Sleep(4000); return 3; });

        Task<int>[] Tasks = new[] { t1, t2, t3 };

        for (int i = 0; i < Tasks.Length; i++)
        {
            Tasks[i].ContinueWith((t) =>
                {
                    if (taskFinishedFlag) return;
                    taskFinishedFlag = true;
                    Console.WriteLine(t.Result);
                }, TaskContinuationOptions.NotOnFaulted);
        }      
Fabjan
  • 13,506
  • 4
  • 25
  • 52
  • 2
    This is the best approach. Instead of writing to the console use a TaskCompletionSource to generate a task that can be awaited on. – usr Jun 25 '15 at 14:16