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

时间:2019-02-26 19:17:01

标签: async-await tcpclient networkstream cancellationtokensource

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

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

public MyView(string id)
{
    InitializeComponent();

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

上面,我的View进行了初始化,然后从Autofac DiC解析MyViewModel,然后调用MyViewModel Init()方法在VM上进行一些附加设置。

然后,Init方法调用我的Async方法GetDataAsync,该方法将返回一个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和Async编程的新手,所以我也不确定在这里是否正确执行取消和异常处理吗?

更新和修复

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

https://devblogs.microsoft.com/pfxteam/await-and-ui-and-deadlocks-oh-my/,作者:Stephen Taub https://montemagno.com/c-sharp-developers-stop-calling-dot-result/,作者James Montemagno https://msdn.microsoft.com/en-us/magazine/jj991977.aspx,作者:StephenCleary https://blog.xamarin.com/getting-started-with-async-await/,作者:乔恩·戈德堡(Jon Goldberger)

@StephenCleary对理解该问题很有帮助。呼叫ResultWait(上面,我在呼叫Result时呼叫GetDataAsync)会导致死锁。

上下文线程(在这种情况下为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);
    }
});

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

2 个答案:

答案 0 :(得分:2)

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

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

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

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

答案 1 :(得分:0)

如我的更新所述,通过提供如下所示的异步lambda来实现全程异步为我解决了问题

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

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