简体   繁体   English

为什么这个异步代码有时会失败,只有在没有观察到的情况下?

[英]Why does this async code sometimes fail, and only when not observed?

This is the original code that had been running fine for a few weeks. 这是几周来一直运行良好的原始代码。 In a test I just did, it failed 0 out of 100 attempts. 在我刚刚做的测试中,它在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);
}

This code failed 9 out of 100 attempts, where one or both of the tuple values is null . 此代码在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);
}

Modify the code to look like this, and it will again fail 0 out of 100 attempts. 修改代码看起来像这样,它将再次失败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);
}

I've been looking at this for over half an hour but I don't see where the mistake is. 我一直在看这个超过半个小时,但我没有看到错误在哪里。 Does anyone see it? 有人看到了吗?

This code: 这段代码:

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

returns a task, but that task is never awaited (and no Continuation is added to it). 返回一个任务,但永远不会等待该任务(并且不会向其添加Continuation)。 So the item's might not get set before Task.WhenAll returns. 因此,在Task.WhenAll返回之前,可能无法设置项目。

However, the original solution seems to have the same problem. 但是,原始解决方案似乎有同样的问题。

My guess is that you are dealing with value types, and that both have a race condition, but in the 2nd example, you copy the value types early enough (while they are still their default value) into the Tuple. 我的猜测是你正在处理值类型,并且两者都有竞争条件,但在第二个例子中,你很早就将值类型(虽然它们仍然是它们的默认值)复制到元组中。 Where as in your other examples you wait long enough before copying them or using them such that the problem continuation that sets the values has run. 凡在你的其他的例子,你等待足够长的时间将其复制或使用它们,这样的问题延续,设置值已经用完了。

To address the comment from to your other question , you very rarely need to mix async / await with ContinueWith . 要解决您对其他问题的评论,您很少需要将async / awaitContinueWith混合使用。 You can do the "fork" logic with help of async lambdas, eg, the code from the question may look like this: 您可以在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;

    // ...
}

Edit: unaccepting my own answer, but leaving it for reference. 编辑:不接受我自己的答案,但留待参考。 The code works , with a catch: ContinueWith loses the SynchronizationContext 代码工作 ,带有catch: ContinueWith失去SynchronizationContext


Thanks to @jbl and @MattSmith for putting me on the right track. 感谢@jbl@MattSmith让我走上正轨。

The problem was indeed that Task.WhenAll does not wait on the continuations. 问题确实是Task.WhenAll不等待继续。 The solution is to set TaskContinuationOptions.AttachedToParent . 解决方案是设置TaskContinuationOptions.AttachedToParent

So this 所以这

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);
        });
}

becomes this 变成了这个

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);
}

More info available on MSDN: Nested Tasks and Child Tasks 有关MSDN的更多信息:嵌套任务和子任务

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM