我有一些代码可以做这样的事情:
abstract class Data
{
Data(string name, bool load) { if (load) { Load().Wait(); }
abstract Task Load();
}
class XmlData : Data
{
XmlData(string name, bool load = true) : base(name, load) {}
override async Task Load()
{
var file = await GetFileAsync(...);
var xmlDoc = await LoadXmlDocAsync(file);
ProcessXml(xmlDoc);
}
void ProcessXml(XmlDocument xmlDoc)
{
foreach (var element in xmlDoc.Nodes)
{
if (element.NodeName == "something")
new XmlData(element.NodeText);
}
}
}
我似乎(有时)会遇到奇怪的计时问题,最终会将代码挂在GetFileAsync(...)上。这是由呼叫的递归性质引起的吗?当我更改所有await调用实际执行.Wait()以使它们完成时,基本上摆脱了调用的所有异步性质,我的代码执行正常。
答案 0 :(得分:3)
这是由呼叫的递归性质引起的吗?当我更改所有await调用实际执行.Wait()以使它们完成时,基本上摆脱了调用的所有异步性质,我的代码执行正常。
这真的取决于 -
最可能的罪魁祸首是,如果您的调用者以某种方式阻止用户界面线程(通过调用Wait()等)。在这种情况下,await的默认行为是捕获调用同步上下文,并将结果发布回该上下文。
但是,如果调用者正在使用该上下文,则可能会出现死锁。
很可能是这种情况,并且是由这行代码引起的:
Data(string name, bool load) { if (load) { Load.Wait(); }
通过使您的库代码(如此XmlData类)显式不使用调用同步上下文,可以轻松避免这种情况。这通常仅对用户界面代码是必需的。通过避免捕获,您可以做两件事。首先,您可以提高整体性能(通常是显着的),其次,您可以避免这种死锁情况。
这可以通过使用ConfigureAwait并更改代码来完成:
override async Task Load()
{
var file = await GetFileAsync.(...).ConfigureAwait(false);
var xmlDoc = await LoadXmlDocAsync(file).ConfigureAwait(false);
ProcessXml(xmlDoc);
}
话虽如此 - 我会重新考虑这个设计。这里真的有两个问题。
首先,你在构造函数中调用虚拟方法,这是非常危险的,应尽可能避免,因为它可能导致异常问题。
其次,将整个异步操作转换为同步操作,方法是将它放在带有块的构造函数中。相反,我会建议重新思考这一切。
也许你可以重做这个来制作某种形式的工厂,它会异步加载你的数据?这可以像创建公共api一样简单,使用返回Task<Data>
的工厂方法,甚至是通用public async Task<TData> Create<TData>(string name) where TData : Data
方法,这样可以保持构造和加载异步并完全避免阻塞