【问题标题】:Why does this async code sometimes fail, and only when not observed?为什么这个异步代码有时会失败,而且只有在没有观察到的时候才会失败?
【发布时间】:2014-04-22 22:13:23
【问题描述】:

这是几个星期以来运行良好的原始代码。在我刚刚进行的测试中,100 次尝试失败了 0 次。

using (var httpClient = new HttpClient())
{
    var tasks = new List<Task>();

    tasks.Add(httpClient.GetAsync(new Uri("..."))
        .ContinueWith(request =>
        {
            request.Result.Content.ReadAsAsync<IEnumerable<Foo>>()
                .ContinueWith(response =>
                {
                    foos = response.Result;
                });
        }));

    tasks.Add(httpClient.GetAsync(new Uri("..."))
        .ContinueWith(request =>
        {
            request.Result.Content.ReadAsAsync<Bar>()
                .ContinueWith(response =>
                {
                    bar = response.Result;
                });
        }));

    await Task.WhenAll(tasks);
}

此代码在 100 次尝试中失败了 9 次,其中一个或两个元组值是 null

var APIresponses = await HttpClientHelper.GetAsync
    <
        IEnumerable<Foo>,
        Bar
    >
    (
        new Uri("..."),
        new Uri("...")
    );

foos = APIresponses.Item1;
bar = APIresponses.Item2;
private static Task GetAsync<T>(HttpClient httpClient, Uri URI, Action<Task<T>> continuationAction)
{
    return httpClient.GetAsync(URI)
        .ContinueWith(request =>
        {
            request.Result.EnsureSuccessStatusCode();

            request.Result.Content.ReadAsAsync<T>()
                .ContinueWith(continuationAction);
        });
}

public static async Task<Tuple<T1, T2>> GetAsync<T1, T2>(Uri URI1, Uri URI2)
{
    T1 item1 = default(T1);
    T2 item2 = default(T2);

    var httpClient = new HttpClient();
    var tasks = new List<Task>()
    {
        GetAsync<T1>(httpClient, URI1, response =>
        {
            item1 = response.Result;
        }),
        GetAsync<T2>(httpClient, URI2, response =>
        {
            item2 = response.Result;
        })
    };

    await Task.WhenAll(tasks);

    return Tuple.Create(item1, item2);
}

将代码修改为如下所示,它会再次失败 100 次尝试中的 0 次。

    await Task.WhenAll(tasks);
    System.Diagnostics.Debug.WriteLine("tasks complete");
    System.Diagnostics.Debug.WriteLine(item1);
    System.Diagnostics.Debug.WriteLine(item2);

    return Tuple.Create(item1, item2);
}

我已经看了半个多小时了,但我看不出错误在哪里。有人看吗?

【问题讨论】:

  • 听起来像是某种竞争条件...(对不起,我帮不上忙!)
  • @DaveDev 不用担心,至少你确认了我同事所说的 :)
  • HttpClient 不是线程安全的。您是否尝试过为每个请求使用单独的实例?
  • @svick 根据MSDNGetAsync 是线程安全的方法之一。我会看看单独的实例会发生什么。
  • @Stijn 哦,我看了“任何实例成员都不能保证是线程安全的”。部分,并没有注意到线程安全方法的列表。他们本可以更清楚地说明这一点。

标签: c# async-await


【解决方案1】:

要将评论从您的other question 发送到您的async/awaitContinueWith,您很少需要混合使用。您可以在async lambdas 的帮助下执行“fork”逻辑,例如,问题中的代码可能如下所示:

using (var httpClient = new HttpClient())
{
    Func<Task<IEnumerable<Foo>>> doTask1Async = async () =>
    {
        var request = await httpClient.GetAsync(new Uri("..."));
        return response.Content.ReadAsAsync<IEnumerable<Foo>>();
    };

    Func<Task<IEnumerable<Bar>>> doTask2Async = async () =>
    {
        var request = await httpClient.GetAsync(new Uri("..."));
        return response.Content.ReadAsAsync<IEnumerable<Bar>>();
    };

    var task1 = doTask1Async();
    var task2 = doTask2Async();

    await Task.WhenAll(task1, task2);

    var result1 = task1.Result;
    var result2 = task2.Result;

    // ...
}

【讨论】:

    【解决方案2】:

    这段代码:

            request.Result.Content.ReadAsAsync<T>()
                .ContinueWith(continuationAction);
    

    返回一个任务,但从不等待该任务(并且没有继续添加它)。所以在Task.WhenAll 返回之前,该项目可能不会被设置。

    但是,原来的解决方案似乎有同样的问题。

    我的猜测是您正在处理值类型,并且两者都有竞争条件,但是在第二个示例中,您足够早地将值类型(虽然它们仍然是它们的默认值)复制到元组中。在您的其他示例中,您在复制它们或使用它们之前等待足够长的时间,以便设置值的问题延续已经运行。

    【讨论】:

    • 它们是引用类型,但我认为你在正确的轨道上。连同@jbl 所说的,我认为Task.WhenAll 确实等待根任务,而不是它们的延续。我会做更多的研究。
    【解决方案3】:

    编辑:不接受我自己的答案,但将其留作参考。代码工作,有一个问题:ContinueWith loses the SynchronizationContext


    感谢@jbl@MattSmith 让我走上正轨。

    问题确实是Task.WhenAll 不等待继续。解决方法是设置TaskContinuationOptions.AttachedToParent

    所以这个

    private static Task GetAsync<T>(HttpClient httpClient, Uri URI, Action<Task<T>> continuationAction)
    {
        return httpClient.GetAsync(URI)
            .ContinueWith(request =>
            {
                request.Result.EnsureSuccessStatusCode();
    
                request.Result.Content.ReadAsAsync<T>()
                    .ContinueWith(continuationAction);
            });
    }
    

    变成这样

    private static Task GetAsync<T>(HttpClient httpClient, Uri URI, Action<Task<T>> continuationAction)
    {
        return httpClient.GetAsync(URI)
            .ContinueWith(request =>
            {
                request.Result.EnsureSuccessStatusCode();
    
                request.Result.Content.ReadAsAsync<T>()
                    .ContinueWith(continuationAction, TaskContinuationOptions.AttachedToParent);
            }, TaskContinuationOptions.AttachedToParent);
    }
    

    更多信息请访问MSDN: Nested Tasks and Child Tasks

    【讨论】:

      猜你喜欢
      • 2021-06-29
      • 1970-01-01
      • 1970-01-01
      • 2010-12-15
      • 1970-01-01
      • 1970-01-01
      • 2016-05-06
      • 1970-01-01
      • 2014-05-01
      相关资源
      最近更新 更多