【问题标题】:How to get results when use Task.WhenAll使用 Task.WhenAll 时如何获取结果
【发布时间】:2019-02-10 16:04:26
【问题描述】:

假设我有一个添加帐户的网络服务。我应该使用此服务来添加帐户列表:

"40701", "40702", "40703", "40704", "40705"

出于测试目的,我尝试模拟此服务的不稳定工作,特别是在第一次尝试添加前三个帐户时,其他两个帐户进入第二轮的情况。第二次尝试只添加“40704”账号,“40705”账号进入第三轮,第三次添加。

public class AddingAccounts
{
    int triesCount = 0;

    // decision table to add accounts
    readonly int[][] dt =
    {
        new int[] { 1, 2, 3 },
        new int[] { 4 },
        new int[] { 5 }
    };

    List<int> result = new List<int>();

    public async Task<List<string>> GetAccountsAsync()
    {
        await Task.Delay(1500);
        return new List<string> { "40701", "40702", "40703", "40704", "40705" };
    }

    public async Task<int> AddAccount(string account)
    {
        try
        {
            await Task.Delay(1000);

            // define accounts at the current attempt
            var accountsToAdd = dt[triesCount].Select(x => $"4070{x}");

            if (accountsToAdd.Contains(account))
            {
                // simulate successful operation, return id account
                return new Random().Next(100);
            }
            else
            {
                throw new InvalidOperationException($"Account {account} was not added");
            }
        }
        catch (Exception ex)
        {
            ex.Data["account"] = account; 
            throw;
        }
    }

    public async Task<List<int>> AddAccountsAsync(List<string> accounts)
    {
        var tasks = accounts.Select(ac => AddAccount(ac));
        Task<int[]> allTasks = Task.WhenAll(tasks);

        try
        {
            var res = await allTasks; 
            result.AddRange(res);  
        }
        catch
        {
            // how can I add returned values of successfully completed tasks to result variable here ?
            // I tried to use tasks variable as John advised
            foreach (var t in tasks)
            {
                // but most tasks have WaitingForActivation status and Result of 0
                if (t.Status == TaskStatus.RanToCompletion)
                {
                    result.Add(t.Result);
                }
            }

            List<string> failedToAddAccounts = new List<string>();

            AggregateException ae = allTasks.Exception;
            foreach(var ex in ae.Flatten().InnerExceptions)
            {                   
                if (ex.Data["account"] is string failedAccount)
                {
                    failedToAddAccounts.Add(failedAccount);
                }
            }

            triesCount++;
            return await AddAccountsAsync(failedToAddAccounts);
        }

        return result;
    }
}

我想获取所有五个帐户的 ID。

如何获得try/catch块中成功完成任务的结果?我的意思是在第一轮等待allTasksallTasks 的状态为Faulted,我无法获得第一个添加帐户的返回值。

【问题讨论】:

  • 您已经在tasks 中拥有原始任务。到allTasks 完成时,所有这些都将完成(无论是否成功),所以使用这些任务。
  • 你为什么有new Random().Next(100)?你知道这很可能会导致重复的数字不是随机的吗? new Random() 应始终为每个线程创建一次以避免此错误。
  • 通过这行代码,我只返回了一些 id。正如我所说,我模拟了添加帐户并返回其 ID 的 Web 服务方法。所以,实际上,它们是否独特并不重要。无论如何,谢谢你的评论,我不知道。

标签: c# async-await task task-parallel-library


【解决方案1】:

您应该为此考虑 Microsoft 的反应式框架 (Rx)。它比使用任务更容易、更强大。

首先,为了更简单,我重写了您的测试代码:

public async Task<List<string>> GetAccountsAsync()
{
    await Task.Delay(1500);
    return new List<string> { "40701", "40702", "40703", "40704", "40705" };
}

private Random _rnd = new Random();

public async Task<int> AddAccount(string account)
{
    await Task.Delay(1000);

    if (_rnd.NextDouble() > 0.5)
    {
        return _rnd.Next(100);
    }
    else
    {
        Console.WriteLine("!");
        throw new InvalidOperationException($"Account {account} was not added");
    }
}

现在,使用 Rx 是一件轻而易举的事:

var query =
    from accounts in Observable.FromAsync(() => GetAccountsAsync())
    from account in accounts.ToObservable()
    from id in Observable.Defer(() => Observable.FromAsync(() => AddAccount(account))).Retry(3)
    select new { account, id };

它像 LINQ 一样被延迟评估,所以要执行它,你可以这样做:

IDisposable subscription =
    query
        .Subscribe(
            result => Console.WriteLine($"Account {result.account} created with id {result.id}"),
            ex => Console.WriteLine($"Exception {ex.GetType().FullName} with \"{ex.Message}\"."),
            () => Console.WriteLine("Completed Successfully"));

要在执行自然完成之前取消执行,只需调用subscription.Dispose()

这里有几个示例运行:

有错误

使用 id 63 创建的帐户 40705 扔在40704上! 使用 id 21 创建的帐户 40701 使用 id 21 创建的帐户 40702 使用 id 27 创建的帐户 40703 扔在40704上! 扔在40704上! 带有“未添加帐户 40704”的异常 System.InvalidOperationException。

成功完成

扔在40703上! 扔上40702! 使用 id 25 创建的帐户 40701 使用 id 88 创建的帐户 40704 使用 id 26 创建的帐户 40705 使用 id 43 创建的帐户 40703 使用 id 98 创建的帐户 40702 成功完成

请注意,有一些错误,但 .Retry(3) 操作员只是简单地尝试创建帐户并最终成功。

只需 NuGet "System.Reactive" 并使用命名空间 System.Reactive.Linq 即可使其正常工作。

【讨论】:

  • 哇!这很酷!以及如何获取已添加帐户的 id 列表?
  • @DmitryStepanov - 查询已经一次返回一个 ID。如果您想要一次全部获得它们,您可以在查询末尾添加 .ToArray(),然后您将在查询结束时获得所有结果。
  • 非常感谢!您的回答鼓励了我开始学习 Rx。
  • @DmitryStepanov - 它非常强大。如果您有任何问题,请告诉我。
  • @DmitryStepanov - 看看Observable.DelayObservable.Catch 他们可能会有所帮助。
猜你喜欢
  • 1970-01-01
  • 2014-06-04
  • 1970-01-01
  • 1970-01-01
  • 2015-01-30
  • 2011-01-29
  • 2017-04-11
  • 1970-01-01
  • 2019-01-24
相关资源
最近更新 更多