En mi aplicación de consola C#, intento ejecutar varias tareas que realizan varias comprobaciones de datos simultáneamente. Si una de las tareas devuelve verdadero, debo detener las otras tareas ya que tengo mi resultado procesable. También es muy posible que ninguna de las funciones devuelva verdadero
Tengo el código para ejecutar las tareas juntas (creo), solo tengo problemas para llegar a la meta:
Task task1 = Task.Run(() => Task1(stoppingToken)); Task task2 = Task.Run(() => Task2(stoppingToken)); Task task3 = Task.Run(() => Task3(stoppingToken)); Task task4 = Task.Run(() => Task4(stoppingToken)); Task task5 = Task.Run(() => Task5(stoppingToken)); Task task6 = Task.Run(() => Task6(stoppingToken)); Task.WaitAll(task1, task2, task3, task4, task5, task6);
Esto es un poco diferente a la respuesta en la pregunta vinculada donde se conoce el resultado deseado (valor de tiempo de espera). Estoy esperando que alguna de estas tareas vuelva a ser verdadera y luego cancele las tareas restantes si aún se están ejecutando.
Task.WhenAny con cancelación de las tareas no completadas y tiempo de espera
Suponiendo que sus tareas devuelvan bool
, puede hacer algo como esto:
CancellationTokenSource source = new CancellationTokenSource(); CancellationToken stoppingToken = source.Token; Task<bool> task1 = Task.Run(() => Task1(stoppingToken)); .... var tasks = new List<Task<bool>> { task1, task2, task3, ... }; bool taskResult = false; do { var finished = await Task.WhenAny(tasks); taskResult = finished.Result; tasks.Remove(finished); } while (tasks.Any() && !taskResult); source.Cancel();
Aquí hay una solución basada en tareas de continuación. La idea es agregar tareas de continuación a cada una de las tareas originales (proporcionadas) y verificar el resultado allí. Si es una coincidencia, la fuente de finalización se establecerá con un resultado (si no hay ninguna coincidencia, el resultado no se establecerá en absoluto).
Luego, el código esperará a lo que suceda primero: se completarán todas las tareas de continuación o se establecerá el resultado de la finalización de la tarea. De cualquier manera, estaremos listos para verificar el resultado de la tarea asociada con la fuente de finalización de la tarea (es por eso que esperamos que se completen las tareas de continuación , no las tareas originales) y si está configurado, es más o menos una indicación de que tenemos una coincidencia (el control adicional al final es un poco paranoico, pero supongo que es mejor prevenir que curar... :D)
public static async Task<bool> WhenAnyHasResult<T>(Predicate<T> isExpectedResult, params Task<T>[] tasks) { const TaskContinuationOptions continuationTaskFlags = TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.AttachedToParent; // Prepare TaskCompletionSource to be set only when one of the provided tasks // completes with expected result var tcs = new TaskCompletionSource<T>(); // For every provided task, attach a continuation task that fires // once the original task was completed var taskContinuations = tasks.Select(task => { return task.ContinueWith(x => { var taskResult = x.Result; if (isExpectedResult(taskResult)) { tcs.SetResult(taskResult); } }, continuationTaskFlags); }); // We either wait for all the continuation tasks to be completed // (it's most likely an indication that none of the provided tasks completed with the expected result) // or for the TCS task to complete (which means a failure) await Task.WhenAny(Task.WhenAll(taskContinuations), tcs.Task); // If the task from TCS has run to completion, it means the result has been set from // the continuation task attached to one of the tasks provided in the arguments var completionTask = tcs.Task; if (completionTask.IsCompleted) { // We will check once more to make sure the result is set as expected // and return this as our outcome var tcsResult = completionTask.Result; return isExpectedResult(tcsResult); } // TCS result was never set, which means we did not find a task matching the expected result. tcs.SetCanceled(); return false; }
Ahora, el uso será el siguiente:
static async Task ExampleWithBooleans() { Console.WriteLine("Example with booleans"); var task1 = SampleTask(3000, true); var task2 = SampleTask(5000, false); var finalResult = await TaskUtils.WhenAnyHasResult(result => result == true, task1, task2); // go ahead and cancel your cancellation token here Console.WriteLine("Final result: " + finalResult); Debug.Assert(finalResult == true); Console.WriteLine(); }
Lo bueno de ponerlo en un método genérico es que funciona con cualquier tipo, no solo booleanos, como resultado de la tarea original.
Podría usar un método asíncrono que envuelva un Task<bool>
en otro Task<bool>
y cancele un CancellationTokenSource
si el resultado de la tarea de entrada es true
. En el ejemplo siguiente, este método es IfTrueCancel
y se implementa como una función local . De esta manera, captura CancellationTokenSource
y, por lo tanto, no tiene que pasarlo como argumento en cada llamada:
var cts = new CancellationTokenSource(); var stoppingToken = cts.Token; var task1 = IfTrueCancel(Task.Run(() => Task1(stoppingToken))); var task2 = IfTrueCancel(Task.Run(() => Task2(stoppingToken))); var task3 = IfTrueCancel(Task.Run(() => Task3(stoppingToken))); var task4 = IfTrueCancel(Task.Run(() => Task4(stoppingToken))); var task5 = IfTrueCancel(Task.Run(() => Task5(stoppingToken))); var task6 = IfTrueCancel(Task.Run(() => Task6(stoppingToken))); Task.WaitAll(task1, task2, task3, task4, task5, task6); async Task<bool> IfTrueCancel(Task<bool> task) { bool result = await task.ConfigureAwait(false); if (result) cts.Cancel(); return result; }
Otra solución bastante diferente a este problema podría ser usar PLINQ en lugar de Task
s creadas explícitamente. PLINQ requiere un IEnumerable
de algo para hacer un trabajo paralelo en él, y en su caso, este algo son las funciones Task1
, Task2
, etc. que desea invocar. Podría ponerlos en una matriz deFunc<CancellationToken, bool>
y resolver el problema de esta manera:
var functions = new Func<CancellationToken, bool>[] { Task1, Task2, Task3, Task4, Task5, Task6 }; bool success = functions .AsParallel() .WithDegreeOfParallelism(4) .Select(function => { try { bool result = function(stoppingToken); if (result) cts.Cancel(); return result; } catch (OperationCanceledException) { return false; } }) .Any(result => result);
La ventaja de este enfoque es que puede configurar el grado de paralelismo y no tiene que depender de la disponibilidad de ThreadPool
para limitar la concurrencia de toda la operación. La desventaja es que todas las funciones deben tener la misma firma. Podría superar esta desventaja declarando las funciones como expresiones lambda como esta:
var functions = new Func<CancellationToken, bool>[] { ct => Task1(arg1, ct), ct => Task2(arg1, arg2, ct), ct => Task3(ct), ct => Task4(arg1, arg2, arg3, ct), ct => Task5(arg1, ct), ct => Task6(ct) };