使用 CancellationTokenSource 时 NetworkStream ReadAsync 和 WriteAsync 无限挂起 - 由 Task.Result(或 Task.Wait)引起的死锁

NetworkStream ReadAsync and WriteAsync hang infinitelly when using CancellationTokenSource - Deadlock Caused by Task.Result (or Task.Wait)

在阅读了关于 Stack Overflow 的几乎所有问题和 Microsoft 关于 NetworkStream 的文档之后,我不明白我的代码有什么问题。

我看到的问题是我的方法 GetDataAsync() 经常挂起。我从 Init Method 中调用此方法,如下所示:

public MyView(string id)
{
    InitializeComponent();

    MyViewModel myViewModel = session.Resolve<MyViewModel>(); //Autofac
    myiewModel.Init(id);
    BindingContext = myViewModel;
}

上面,我的视图进行初始化,然后从 Autofac DiC 解析 MyViewModel,然后调用 MyViewModel Init() 方法在 VM 上做一些额外的设置。

Init 方法然后调用我的异步方法 GetDataAsync,其中 return 一个 IList,如下所示:

public void Init()
{
    // call this Async method to populate a ListView
    foreach (var model in GetDataAsync("111").Result)
    {
        // The List<MyModel> returned by the GetDataAsync is then
        // used to load ListView's ObservableCollection<MyModel>
        // This ObservableCollection is data-bound to a ListView in
        // this View.  So, the ListView shows its data once the View
        // displays.
    }
}

,这是我的 GetDataAsync() 方法,包括我的评论:

public override async Task<IList<MyModel>> GetDataAsync(string id)
{
    var timeout = TimeSpan.FromSeconds(20);

    try
    {
        byte[] messageBytes = GetMessageBytes(Id);

        using (var cts = new CancellationTokenSource(timeout))
        using (TcpClient client = new TcpClient(Ip, Port))
        using (NetworkStream stream = client.GetStream())
        {
            await stream.WriteAsync(messageBytes, 0, messageBytes.Length, cts.Token);
            await stream.FlushAsync(cts.Token);

            byte[] buffer = new byte[1024];
            StringBuilder builder = new StringBuilder();
            int bytesRead = 0;

            await Task.Delay(500);                 
            while (stream.DataAvailable) // need to Delay to wait for data to be available
            {
                bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cts.Token);
                builder.AppendFormat("{0}", Encoding.ASCII.GetString(buffer, 0, bytesRead));
            }

            string msg = buffer.ToString();
        }

        return ParseMessageIntoList(msg);  // parses message into IList<MyModel>
    }
    catch (OperationCanceledException oce)
    {
        return await Task.FromResult<IList<RoomGuestModel>>(new List<RoomGuestModel>());
    }
    catch (Exception ex)
    {
        return await Task.FromResult<IList<RoomGuestModel>>(new List<RoomGuestModel>());
    }
}

我希望 ReadAsync 或 WriteAsync 要么成功完成,要么抛出一些异常,要么在 10 秒后被取消,在这种情况下我会捕获 OperationCanceledException。

但是,当我调用上面的方法时,它会一直挂起。如果我正在调试并且在上面的代码中有一些断点,我将能够完整地执行该方法,但是如果我第二次调用它,应用程序就会永远挂起。

我是 Tasks 和异步编程的新手,所以我也不确定我是否在此处正确地执行了取消和异常处理?

更新并修复

我想出了解决死锁问题的方法。希望这会帮助其他人可能 运行 遇到同样的问题,我会先解释一下。对我帮助很大的文章是:

https://devblogs.microsoft.com/pfxteam/await-and-ui-and-deadlocks-oh-my/ 作者:斯蒂芬·陶布 https://montemagno.com/c-sharp-developers-stop-calling-dot-result/ 作者:詹姆斯·蒙特马格诺 https://msdn.microsoft.com/en-us/magazine/jj991977.aspx 来自 StephenCleary https://blog.xamarin.com/getting-started-with-async-await/ 作者:乔恩·戈德伯格

@StephenCleary 对理解这个问题很有帮助。调用ResultWait(上面我调用GetDataAsync时调用Result会导致死锁

上下文线程(UI 在这种情况下)现在正在等待 GetDataAsync 完成,但是 GetDataAsync 捕获当前上下文线程(UI 线程) ,因此它可以在从 TCP 获取数据后恢复。但是由于这个上下文线程现在被调用 Result 阻塞,它无法恢复。

最终结果是调用 GetDataAsync 看起来死锁了,但实际上,调用 Result 死锁了。

在阅读了@StephenTaub、@StephenCleary、@JamesMontemagno、@JoeGoldenberger 的大量文章后(谢谢大家),我开始理解这个问题(我是 TAP/async/await 的新手)。

然后我发现了 Tasks 中的延续以及如何使用它们来解决问题(感谢上面 Stephen Taub 的文章)。

所以,与其这样称呼它:

IList<MyModel> models = GetDataAsync("111").Result;
foeach(var model in models)
{
  MyModelsObservableCollection.Add(model);
}

,我这样称呼它:

GetDataAsync(id)
    .ContinueWith((antecedant) =>
    {
        foreach(var model in antecedant.Result)
        {
            MyModelsObservableCollection.Add(model);
        }

    }, TaskContinuationOptions.OnlyOnRanToCompletion)
    .ContinueWith((antecedant) =>
    {
        var error = antecedant.Exception.Flatten();
    }, TaskContinuationOptions.OnlyOnFaulted);

This seam to have fixed my deadlocking issue and now my list will load fine even though it is loaded from the constructor.  

所以,这个接缝工作得很好。但@JoeGoldenberger 在他的文章 https://blog.xamarin.com/getting-started-with-async-await/ 中也提出了另一种解决方案,即使用 Task.Run(async()=>{...}); 并在其中等待 GetDataAsync 并加载 ObservableCollection。所以,我也试了一下,也没有阻塞,所以效果很好:

Task.Run(async() =>  
{
    IList<MyModel> models = await GetDataAsync(id);
    foreach (var model in models)
    {
        MyModelsObservableCollection.Add(model);
    }
});

所以,看起来这两个中的任何一个都可以很好地消除死锁。并且由于上面我的 Init 方法是从 c-tor 调用的;因此,我不能让它异步并等待,使用上述两种方法之一解决了我的问题。我不知道哪个更好,但在我的测试中,它们确实有效。

您的问题很可能是由于 GetDataAsync("111").Result。你shouldn't block on async code.

这可能会导致死机。例如,如果您在 UI 线程上,则 UI 线程将启动 GetDataAsync 和 运行,直到遇到 await。此时,GetDataAsync returns 一个未完成的任务,.Result 调用会阻塞 UI 线程,直到该任务完成。

最终,内部异步调用完成并且 GetDataAsync 准备好在其 await 之后恢复执行。默认情况下,await 捕获其上下文并在该上下文中恢复。在这个例子中是 UI 线程。自调用 Result 以来已被阻止。因此,UI 线程正在等待 GetDataAsync 完成,而 GetDataAsync 正在等待 UI 线程以便它可以完成:死锁。

正确的解决办法是去async all the way;将 .Result 替换为 await,并对其他代码进行必要的更改。

如我的更新所述,通过提供如下所示的异步 lambda 来一直异步解决了我的问题

Task.Run(async() =>  
{
    IList<MyModel> models = await GetDataAsync(id);
    foreach (var model in models)
    {
        MyModelsObservableCollection.Add(model);
    }
});

以这种方式在 ctor 中异步加载一个可观察集合(在我的例子中,ctor 调用 Init 然后使用这个 Task.Run)解决了问题