-1

I have to execute a certain number of methods, and the first method that ends with a certain value must exit the loop.

The code looks like this:

var tasks = new List<Task<bool>>();
tasks.Add(VerificheHandler.RandomString(14));
tasks.Add(VerificheHandler.RandomString(9000)); 

public bool Test()
{
    Parallel.ForEach(tasks, task =>
    {
        task.Start();
        if(task.Result)
        {
            //exit parallel and kill all
            return true;
        }
    }); 
}   
                    
public async static Task<bool> RandomString(int delay)
{
    await Task.Delay(delay);
    return true;
}

Error is:

task.Result
Start may not be called on a promise-style task
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Fede
  • 9
  • 4
  • 5
    `Parallel.Foreach` is not suitable to run `Task`s in parallel. It was desinged to execute CPU bound operations. – Peter Csala Feb 11 '21 at 11:47
  • `and the first method that ends with a certain value` What certain value? In this case is it `true`? – mjwills Feb 11 '21 at 11:47
  • 3
    yeah, `async` methods always return "hot" `Task`s, you don't call `Start` on them, they've already started. The whole idea of separating task creation from task execution turned out not to be a great idea in general. You *might* be looking for a combination of `(When/Wait)Any` on a set of `Task`s and using a `CancellationToken` but you're entire question at the moment is taken up with code involving the wrong mechanism. – Damien_The_Unbeliever Feb 11 '21 at 11:48
  • 2
    `Task.WhenAny` is likely the right way forward here as Damien says. Call it with the list, and if the result is not what you expect then remove that task from the list and call it again. If it is what you expect, return. – mjwills Feb 11 '21 at 11:52
  • What is the expected result of the `Test` method, in case none of the `tasks` completes with the specific `true` result? – Theodor Zoulias Feb 11 '21 at 15:51
  • Btw I am guessing that what you actually want is this: [Parallel foreach with asynchronous lambda](https://stackoverflow.com/questions/15136542/parallel-foreach-with-asynchronous-lambda) – Theodor Zoulias Feb 11 '21 at 16:18

2 Answers2

1

You can use ParallelLoopState.Break to top execution of a parallel loop early. There is also a .Stop() method. Break will continue execution of all indices up to the index that called break, while stop will not start lower indexed iterations. In both cases the loop may have already started or completed higher indexed iterations.

Also, parallel.for/foreach is usually not combined with tasks. Making the method synchronous would make it look something like this:

    public static void Test()
    {
        Parallel.For(0, 10, (i, state) =>
        {
            var r = RandomString(100);
            if (r)
            {
                state.Break();
            }
        });
    }

    public static bool RandomString(int delay)
    {
        Thread.Sleep(delay);
        return true;
    }
JonasH
  • 28,608
  • 2
  • 10
  • 23
0

Your provided example won't compile, because you can't store Task<bool> in a collection of Task<string>.

If I understand your question properly then you are looking for a solution:

  • To start multiple methods simultaneously
  • Stop the execution whenever one of the methods return with a given value.

In order to implement such a solution we have to use CancellationToken to be able to stop any ongoing operation. So the amended RandomString could look like this:

public static async Task<string> RandomString(int delay, CancellationToken token)
{
    try
    {
        await Task.Delay(delay, token);
    }
    catch (OperationCanceledException) 
    {
        Console.WriteLine("Task has been cancelled " + delay);
        return null;
    }
    Console.WriteLine("Finished " + delay);
    return "Random " + delay;
}
  • I have added a new CancellationToken parameter to the method and then I've passed it to the Task.Delay.
    • If there was no cancellation then it will print out some debug information and return with some random string.
    • If there was a cancellation during Task.Delay then we shallow the OperationCanceledException and print out some debug info.

In order to be able to cold start this method, we could introduce a simple wrapper around it:

public static Func<CancellationToken, Task<string>> RandomStringWrapper(int delay)
    => (ct) => RandomString(delay, ct);
  • It does not call the RandomString right away instead it returns a function which expects a CancellationToken.
  • Whenever a CancellationToken is passed then the Task will run.

So, now we can introduce an extension method which is generic enough to support multiple different async methods:

public static class TaskEx
{
    public static async Task RunUntil<T>(this IEnumerable<Func<CancellationToken, Task<T>>> asyncFunctions, T exitCondition, CancellationTokenSource cts)
    {
        var jobs = asyncFunctions.Select(fn => fn(cts.Token)).ToList();
        while (true)
        {
            var fastest = await Task.WhenAny(jobs);
            if (fastest.Result.Equals(exitCondition))
            {
                cts.Cancel(true);
                return;
            }

            jobs.Remove(fastest);

            if (jobs.Count == 0)
                return;
        }
    }
}
  • asyncFunctions: It is a collection of functions. Each expects a CancellationToken to return a Task<T>.
  • exitCondition: As its name suggests...
  • cts: The CancellationToken provider.
  • First we start the async methods by passing the CancellationToken to them.
  • Inside the loop we wait for the fastest to complete.
  • If the fastest's result is equal to the exitCondition then we cancel the remaining tasks.
  • If they are not equal then we remove the completed job from the jobs and re-run the same loop until there is a job to execute.

Now, let's put all this together:

static async Task Main(string[] args)
{
    var cts = new CancellationTokenSource();
    var tasks = new[] { RandomStringWrapper(14), RandomStringWrapper(600), RandomStringWrapper(500) };
    await tasks.RunUntil("Random 500", cts);
}

The output will be:

Finished 14
Finished 500
Task has been cancelled 600
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • 1
    Peter the `while (true) ... Task.WhenAny` loop is an antipattern. It scales poorly when the number of tasks becomes large, regarding both performance and allocations. It is OKish for small number of tasks (less than 1000), but I would avoid it anyway in a production environment. Also I would avoid swallowing the `OperationCanceledException`s too early, and using magic values (like `null`) to communicate cancellation. – Theodor Zoulias Feb 11 '21 at 16:15
  • 1
    @TheodorZoulias Yepp, your observations are correct. Obviously this is a naive solution for an oversimplified problem. – Peter Csala Feb 11 '21 at 16:27
  • 1
    Peter btw be aware that Microsoft is not happy with having introduced the `TaskCanceledException` type. You can hear Stephen Toub talking [here](https://www.youtube.com/watch?v=JltNo-NKTlQ&t=2551) about it. For this reason I would recommend catching the base type `OperationCanceledException` instead. – Theodor Zoulias Feb 11 '21 at 19:05
  • 1
    @TheodorZoulias Thanks, I've fixed it. – Peter Csala Feb 11 '21 at 21:26