异步/等待链挂起或死锁

本文关键字:死锁 挂起 等待 异步 | 更新日期: 2024-09-21 14:33:30

我有一个通用应用程序,可以从互联网源更新几个对象。为了更新进度指示器并防止重复请求,我有以下逻辑。

刷新按钮:

foreach (var package in Packages.Where(item => !item.Received)) 
{
    RefeshPackage(package);
}

它为列表中的每个包调用RefreshPackage:

private async Task RefeshPackage(PackageModel package)
{
    if (Tasks.Contains(package.Id)) return;    
    Tasks.Add(package.Id);
    await DownloadAndUpdate(package);
    Tasks.Remove(package.Id);
    Refresh.RaiseCanExecuteChanged();
}

为每个包调用DownloadAndUpdate:

private async Task DownloadAndUpdate(PackageModel package)
{
    var response = await webService.GetPackageStages(package.Id);
    if (response != null)
    {
        switch (response.Status)
        {
            case 200:
                package.UpdateStages(response.Stages);
                break;
            case 500:
                //package doesn't exist or website down
                break;
            default:
                break;
        }
    }
    else
    {
        //no network / timed out
    }
}

它为每个包调用GetPackageStages:

public async Task<ResponseData> GetPackageStages(string id)
{
    var requestUri = string.Concat(MyUri, id);
    var client = new HttpClient();
    client.Timeout = new TimeSpan(0,0,5);
    try
    {
        var response = await client.GetAsync(requestUri);
        if(response.StatusCode == System.Net.HttpStatusCode.OK)
        {
            //process response
        }
    catch (Exception e)
    {
        return null;
    }
    finally
    {
        client.Dispose();
    }
}

挂起/死锁总是发生在最后一个函数中,行:

await client.GetAsync(requestUri);

有时它永远挂在这里,有时它工作得很好。我在这里读了很多关于死锁的其他答案,常见的解决方案是一直使用async/await(我已经在做了)或使用。ConfigureAwait(false)也无处不在(我已经做了,但没有成功)。

我还缺少什么吗?还是应该用不同的方式构建代码?我的想法/解决方案都用完了。如有任何帮助,我们将不胜感激。

异步/等待链挂起或死锁

外部foreach只是执行RefeshPackage,而不对其结果执行任何操作。这种"激发并忘记"方法的问题是,异常将不被发现,因此很难判断异步代码是否真的正确执行。即使你的任务应该处理异常,你仍然应该观察它,以防错误处理策略没有涵盖某些内容(在任务中使用catch-all处理程序和在其他地方使用它一样糟糕)。

有许多方法可以使用生成的Task。哪个合适取决于你的情况。如果外循环本身是async,那么await结果就足够了(如果是同步的,则使用Task.Wait())。这意味着在任务完成之前,循环不会继续;如果您实际上想并行运行所有任务,请将它们收集在Task[]中并使用await Task.WhenAll()(对于同步代码,请使用Task.WaitAll())。

如果您希望所有任务并行运行,并且不关心任何结果,那么您至少可以使用Task.ContinueWith(),传递TaskContinuationOptions.OnlyOnFaulted来运行日志记录代码。这是最接近于在不丢弃实际错误的情况下"开火并忘记"的事情。

一旦您修复了这个问题,并且能够观察到异步代码的任何错误,就可以开始调试实际的问题了。