Task.WhenAny for non faulted tasks

前端 未结 2 1495
情深已故
情深已故 2021-01-23 08:08

The description of the Task.WhenAny method says, that it will return the first task finished, even if it\'s faulted. Is there a way to change this behavior, so it w

2条回答
  •  -上瘾入骨i
    2021-01-23 08:39

    First off, from my review there is no direct way of doing this without waiting for all the tasks to complete then find the first one that ran successfully.

    To start with I am not sure of the edge cases that will cause issues that I havent tested, and given the source code around tasks and contiunuation requires more than an hour of review I would like to start to think around the follow source code. Please review my thoughts at the bottom.

    public static class TaskExtensions
    {
    
        public static async Task WhenFirst(params Task[] tasks)
        {
            if (tasks == null)
            {
                throw new ArgumentNullException(nameof(tasks), "Must be supplied");
            }
            else if (tasks.Length == 0)
            {
                throw new ArgumentException("Must supply at least one task", nameof(tasks));
            }
    
            int finishedTaskIndex = -1;
            for (int i = 0, j = tasks.Length; i < j; i++)
            {
                var task = tasks[i];
                if (task == null)
                    throw new ArgumentException($"Task at index {i} is null.", nameof(tasks));
    
                if (finishedTaskIndex == -1 && task.IsCompleted && task.Status == TaskStatus.RanToCompletion)
                {
                    finishedTaskIndex = i;
                }
            }
    
            if (finishedTaskIndex == -1)
            {
                var promise = new TaskAwaitPromise(tasks.ToList());
                for (int i = 0, j = tasks.Length; i < j; i++)
                {
                    if (finishedTaskIndex == -1)
                    {
                        var taskId = i;
    #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
                        //we dont want to await these tasks as we want to signal the first awaited task completed.
                        tasks[i].ContinueWith((t) =>
                        {
                            if (t.Status == TaskStatus.RanToCompletion)
                            {
                                if (finishedTaskIndex == -1)
                                {
                                    finishedTaskIndex = taskId;
                                    promise.InvokeCompleted(taskId);
                                }
    
                            }
                            else
                                promise.InvokeFailed();
                        });
    #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
    
                    }
    
                }
                return await promise.WaitCompleted();
            }
    
    
            return Task.FromResult(finishedTaskIndex > -1 ? tasks[finishedTaskIndex] : null);
        }
    
        class TaskAwaitPromise
        {
            IList _tasks;
            int _taskId = -1;
            int _taskCount = 0;
            int _failedCount = 0;
    
    
            public TaskAwaitPromise(IList tasks)
            {
                _tasks = tasks;
                _taskCount = tasks.Count;
                GC.KeepAlive(_tasks);
            }
    
            public void InvokeFailed()
            {
                _failedCount++;
            }
    
            public void InvokeCompleted(int taskId)
            {
                if (_taskId < 0)
                {
                    _taskId = taskId;
                }
            }
    
            public async Task WaitCompleted()
            {
                await Task.Delay(0);
    
                while (_taskId < 0 && _taskCount != _failedCount)
                {
    
                }
                return _taskId > 0 ? _tasks[_taskId] : null;
            }
    
        }
    
    }
    

    The code is lengthy I understand and may have lots of issues, however the concept is you need to execute all the tasks in parallel and find the first resulting task that completed successfully.

    If we consider that we need to make a continuation block of all the tasks and be able to return out of the continuation block back to the original caller. My main concern (other than the fact I cant remove the continuation) is the while() loop in the code. Probably best to add some sort of CancellationToken and/or Timeout to ensure we dont deadlock while waiting for a completed task. In this case if zero tasks complete we never finish this block.

    Edit I did change the code slightly to signal the promise for a failure so we can handle a failed task. Still not happy with the code but its a start.

提交回复
热议问题