为什么这个异步代码有时会失败,只有在未观察到时才会失败?

4

这是原本的代码,它已经运行了几周的时间。在我的测试中,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
@DaveDev 没问题,至少你证实了我的同事所说的 :) - user247702
HttpClient 不是线程安全的。您尝试过为每个请求使用单独的实例吗? - svick
根据MSDN的说法,GetAsync是线程安全的方法之一。我会尝试使用单独的实例来查看发生了什么。 - user247702
@Stijn 哦,我看了“任何实例成员都不能保证是线程安全的”这部分,没有注意到那个线程安全方法列表。他们本可以更清楚地表达。 - svick
显示剩余5条评论
3个回答

2

这段代码:

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

返回一个任务,但该任务从未被等待(也没有添加连续项)。 因此,在 Task.WhenAll 返回之前,可能不会设置项目。

然而,原始解决方案似乎存在相同的问题。

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


1
它们是引用类型,但我认为你走在了正确的道路上。结合@jbl所说的,我认为Task.WhenAll确实会等待根任务完成,但不会等待它们的继续执行。我会做更多的研究。 - user247702

2
为了回应您在其他问题中的评论,您很少需要将async/awaitContinueWith混合使用。您可以通过使用async lambda来实现“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;

    // ...
}

1

编辑:取消接受自己的答案,但保留作为参考。这段代码有效,但有一个注意点:ContinueWith 失去了同步上下文


感谢@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:嵌套任务和子任务上获得。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接