Mad*_*din 1 .net c# deadlock nlog async-await
这是我正在研究的真实代码的最小(和简化)重现,因为它导致了死锁。有OuterClass一些嵌套类,它们通过外部类的初始化进行初始化(并加载一些数据)。重要的是,一旦对象初始化,数据就可用。
public class OuterClass
{
static ILogger Log = LogManager.GetLogger("OuterClass");
private static InnerClass _innerClass;
public static OuterClass Instance { get; private set; }
private OuterClass()
{
Log.Debug("Constructor called");
_innerClass = new InnerClass();
}
static OuterClass()
{
Instance = new OuterClass();
}
public class InnerClass
{
// using it's own logger, it will not dead lock
//static ILogger Log = LogManager.GetLogger("InnerClass");
private String _data;
public InnerClass()
{
LoadOrUpdateDataAsync().Wait(); // will make the second Log.Debug call dead lock
//_ = LoadOrUpdateDataAsync(); // will work
}
public async Task LoadOrUpdateDataAsync()
{
Log.Debug("This works");
_data = await DataClass.GetDataAsync("https://someusefulapi.com/api/xyz");
Log.Debug("If you see this, it did not block"); // this line blocks
}
}
}
public static class DataClass
{
static ILogger Log = LogManager.GetLogger("DataClass");
public static async Task<string> GetDataAsync(string uri)
{
// just example code, HttpClient should not be in a using block
using (var client = new HttpClient())
{
var data = await client.GetStringAsync(uri);
Log.Debug("Data loaded from {0}", uri);
return data;
}
}
}
Run Code Online (Sandbox Code Playgroud)
上述代码的控制台输出:
Time Logger Thread-Id Message
16:47:39 OuterClass 1 Constructor called
16:47:39 OuterClass 1 This works
16:47:40 DataClass 7 Data loaded from https://someusefulapi.com/api/xyz
Run Code Online (Sandbox Code Playgroud)
第二个日志Log.Debug("If you see this...");块 - 应用程序挂起。我猜测它此时挂起,因为调用LoadOrUpdateDataAsync().Wait();正在阻塞和Logger共享的。由不同的线程(不同的线程id)执行。因此,当等待完成时,该方法将在“新”线程上恢复,但 Logger 被主线程阻塞。或者代码是否真的可以工作,这意味着这是一个 NLog 问题?OuterClassInnerClassawait DataClass.GetDataAsync(...)
NLog 配置非常简约。我尝试了 NLog 4.7.15 和 5.0.4 版本 - 它们的行为相同。选择async="true",也没有什么区别。它是.Net Framework 4.7.2。
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<targets async="true">
<target name="console" xsi:type="ColoredConsole" layout="${date:format=HH\:mm\:ss} ${pad:padCharacter= :padding=10:fixedLength=True:inner=${logger}} ${mdlc:item=ProcessDescriptorId} ${threadid} ${message} ${exception:format=tostring} ${exception:format=stacktrace}" />
</targets>
<rules>
<logger name="*" minlevel="Debug" writeTo="console" />
</rules>
</nlog>
Run Code Online (Sandbox Code Playgroud)
该代码实际上仅在LoadOrUpdateDataAsync()从构造函数调用时挂起。对函数进行调用Init()并用 调用它OuterClass.Instance.Init();不会阻塞。
另外,使用丢弃运算符_ = LoadOrUpdateDataAsync();也可以。但这不是一个选择,因为主线程将继续,而没有信心数据确实已加载。
控制台输出_ = LoadOrUpdateDataAsync();:
Time Logger Thread-Id Message
16:49:54 OuterClass 1 Constructor called
16:49:54 OuterClass 1 This works
16:49:55 DataClass 9 Data loaded from https://someusefulapi.com/api/xyz
16:49:55 OuterClass 9 If you see this, it did not block
Run Code Online (Sandbox Code Playgroud)
还可以在中使用自己的记录器InnerClass(通过调用.Wait())。
控制台输出带有自己的记录器 inInnerClass和LoadOrUpdateDataAsync().Wait();:
Time Logger Thread-Id Message
16:52:51 OuterClass 1 Constructor called
16:52:51 InnerClass 1 This works
16:52:52 DataClass 8 Data loaded from https://someusefulapi.com/api/xyz
16:52:52 InnerClass 8 If you see this, it did not block
Run Code Online (Sandbox Code Playgroud)
从最后两个示例可以清楚地看出,第二个示例Log.Debug()与第一个示例运行在不同的线程上。
我知道从构造函数调用异步函数不是最理想的,并且有更好的方法可以做到这一点 - 例如使用 Stephen Cleary 在本博客中描述 的工厂模式
我主要感兴趣的是为什么这段代码会阻塞,并希望了解这里的技术原理。我的假设是正确的还是我走错了路?
其次,上面提到的工厂模式是解决它的“正确”方法,还是有更好的方法?
我无法查明导致所描述行为的确切代码,因为这将需要深入 CLR 实现的兔子洞(使用dotnet-dump analyze似乎会导致后续调用中的非托管转换_data = await ...),但这种行为的原因并不是NLog 本身,但运行时保证静态构造函数仅执行一次,并且在您的代码中基本上会发生以下情况:
OuterClassstatic ctor 开始调用InnerClass演员被称为InnerClassctor 阻塞LoadOrUpdateDataAsync(并阻塞处理该 ctor 的线程)LoadOrUpdateDataAsync等待IO完成LoadOrUpdateDataAsync访问某些内容,但静态构造尚未完成,因此它会阻塞,直到构造完成,从而导致死锁,因为 static ctor 会等待.OuterClassOuterClassLoadOrUpdateDataAsync重现可以简化为:
public class OuterClass
{
// ...
public static void DoSomething()
{
Console.WriteLine("Here Outer");
}
public class InnerClass
{
private String _data;
public InnerClass()
{
LoadOrUpdateDataAsync().GetAwaiter().GetResult();//.Wait();
}
public async Task LoadOrUpdateDataAsync()
{
_data = await DataClass.GetDataAsync("https://api.coindesk.com/v1/bpi/currentprice.json").ConfigureAwait(false);
Console.WriteLine("Here Inner"); // works
OuterClass.DoSomething(); // blocks
}
}
}
Run Code Online (Sandbox Code Playgroud)
这实际上在文档中进行了描述:
运行时在单个应用程序域中调用静态构造函数不超过一次。该调用是根据类的特定类型在锁定区域中进行的。静态构造函数的主体中不需要额外的锁定机制。为了避免死锁的风险,不要在静态构造函数和初始化程序中阻塞当前线程。例如,不要等待任务、线程、等待句柄或事件,不要获取锁,也不要执行阻塞并行操作(例如并行循环和
Parallel.Invoke并行 LINQ 查询)。
| 归档时间: |
|
| 查看次数: |
340 次 |
| 最近记录: |