使用 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 对理解这个问题很有帮助。调用Result
或Wait
(上面我调用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)解决了问题
在阅读了关于 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 对理解这个问题很有帮助。调用Result
或Wait
(上面我调用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)解决了问题