Click here to Skip to main content
15,881,380 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
I have a list of tasks that I would like to run in parallel. When one completes, I would like to examine the result of that one task. If it is successful, I would like to end all the other tasks, which I think should be no problem using a CancellationToken. If the task is unsuccessful, I'd like to go on waiting for the other tasks.

Can anyone offer advice on how best to approach this?

Having tried to use async/await, I am thinking this may be a case where I need to go back to something more 'manual', perhaps accumulating results in a suitable concurrent collection and building logic around that.

What I have tried:

I've come up with this so far but I don't know what to do in the case where a task completes but is unsuccessful. I'm thinking I need to remove it from the list of tasks that I wait for but this causes an IndexOutOfRangeException. Also, the double use of await worries me because I don't recall ever reading any code that does that.

public static class Program
{
	private const int NumTasks = 10;
	private readonly static int[] TaskDelays = { 5000, 6000, 6000, 6000, 8000, 6000, 10000, 12000, 1000, 22000 };
	private static List<Task<bool>> _tasks = new();

	static async Task Main( string[] args )
	{
		Debug.WriteLine( "****** START ******" );
		await RunAsync();
		Debug.WriteLine( "****** END ******" );
		Console.ReadKey();
	}

	private static async Task RunAsync()
	{
		for( int i = 0; i < NumTasks; ++i )
			_tasks.Add( ReturnDelayedResultAsync( i ) );
		int numCompletedTasks = 0;
		bool gotSuccess = false;
		while( !gotSuccess && numCompletedTasks < NumTasks )
		{
			Debug.WriteLine( $"Awaiting {_tasks.Count} tasks" + TaskStatusString() );
			bool result = await await Task.WhenAny( _tasks );
			++numCompletedTasks;
			if( result )
			{
				Debug.WriteLine( $"Task successfully connected; number of completed tasks = {numCompletedTasks}." );
				gotSuccess = true;
			}
			else
			{
				// Don't know what to do here.
			}
		}
	}

	private static async Task<bool> ReturnDelayedResultAsync( int index )
	{
		Debug.WriteLine( $"Starting task {index}." );
		await Task.Delay( TaskDelays[ index ] );
		Debug.WriteLine( $"Ending task {index}." );
		return index == 6;
	}

	private static string TaskStatusString()
	{
		StringBuilder sb = new StringBuilder( ": " );
		for( int i = 0; i < NumTasks; ++i )
			sb.Append( i.ToString() + "=" + _tasks[ i ].Status.ToString() + "; " );
		sb.Append( Environment.NewLine );
		return sb.ToString();
	}
}
Posted
Updated 9-Nov-21 23:16pm

1 solution

Task.WhenAny will return the first task which completed. You can use that to remove it from the list.
C#
while (!gotSuccess && _tasks.Count != 0)
{
    Task<bool> completedTask = await Task.WhenAny(_tasks);
    _tasks.Remove(completedTask);
    ++numCompletedTasks;
    
    if (await completedTask) // Or: if (completedTask.Result)
    {
        Debug.WriteLine( $"Task successfully connected; number of completed tasks = {numCompletedTasks}." );
        gotSuccess = true;
    }
}

If you want to cancel the other tasks once one task has succeeded, then a CancellationToken is the correct approach:
C#
private static async Task<bool> ReturnDelayedResultAsync( int index, CancellationToken cancellationToken )
{
	Debug.WriteLine( $"Starting task {index}." );
	await Task.Delay( TaskDelays[ index ], cancellationToken );
	Debug.WriteLine( $"Ending task {index}." );
	return index == 6;
}

private static async Task RunAsync()
{
    CancellationTokenSource cts = new();
    for( int i = 0; i < NumTasks; ++i )
        _tasks.Add( ReturnDelayedResultAsync( i, cts.Token ) );

    int numCompletedTasks = 0;
    bool gotSuccess = false;
    while( !gotSuccess && _tasks.Count != 0 )
    {
        Debug.WriteLine( $"Awaiting {_tasks.Count} tasks" + TaskStatusString() );
        Task<bool> completedTask = await Task.WhenAny(_tasks);
        _tasks.Remove(completedTask);
        ++numCompletedTasks;
        
        if (await completedTask) // Or: if (completedTask.Result)
        {
            Debug.WriteLine( $"Task successfully connected; number of completed tasks = {numCompletedTasks}." );
            gotSuccess = true;
            cts.Cancel();
        }
    }
}
 
Share this answer
 
Comments
Patrick Skelton 10-Nov-21 6:57am    
Jeez... that is so simple when you see how it should be done! I was *so* close to that yesterday afternoon but was getting a crash that lead me down a garden path. Thank you for all your help, Richard. For my personal edification, can you suggest a place I could put a call to Debug.WriteLine(TaskStatusString()) that would show any of them as Running?
Richard Deeming 10-Nov-21 7:15am    
That would be tricky; once you await Task.Delay(...), that task moves from Running to WaitingForActivation.

You'd have to somehow catch the task while it's doing something, and not awaiting anything.
BillWoodruff 11-Nov-21 0:40am    
+5

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900