ThreadLocks和静态构造函数

时间:2015-10-07 14:11:21

标签: c# asp.net .net multithreading garbage-collection

假设:

IIS中托管的ASP.net Web api应用程序。 该应用程序为一个插件生成大约30个应用程序域,这些插件可以执行一些外部工作。

该应用程序为很多用户提供服务,并且大部分时间都在运行,但有时(几天甚至几周后)它会突然挂起。

问题:

一个Web应用程序有时会“挂起”,导致需要重新启动w3wp.exe。

在这种状态下对转储进行一些检查后,我们发现在这个时刻有很多线程(有时约为15.000)。

在正常情况下,我们从不会观察超过一百个线程。

DebugDiag说有一个线程阻塞其他线程

enter image description here

现在我们已经在线程44(以及许多其他人;约90%)中看到,最后有相同的电话:

enter image description here

Method本身没有任何锁定或线程行为。但是它的静态构造函数有一个不常见的东西。 ctor看起来像这样:

   static TimeZoneHelper()
        {
        using (StringReader reader = new StringReader(Resources.TimeZones))
        {
            string line;

            while ((line = reader.ReadLine()) != null)
            {
                string[] parts = line.Split(';');

                TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(parts[1]);

                timeZones[parts[0]] = timeZone;
            }
        }
}

此外,调试分析表明应用程序处于活动状态gc(正如您可能会问的那样:我们永远不会手动触发gc.collect) enter image description here

问题 是否有证据表明此类代码在静态ctor中存在问题?即使没有任务或线程代码?也许与GC进展本身有关的东西(因为对象是一次性的,即使没有处置代码?)

TimeZoneHelper

我创建了一个gist,其中包含了这个类的主要方法,其中包括ctor和名为TimeZoneHelper.ToTimeZoneOffset的方法:

https://gist.github.com/Gentlehag/9d564555261da0e73366

该方法导致的主要内容是Dictionary.TryGet(在ctor中创建)

编辑 顺便说一下,我还想在每个appdomain中添加一个程序集解析事件。 代码可以在这里看到:

https://gist.github.com/Gentlehag/4726b6d888adb149684d

重要更新 我是同事,只想添加更多信息。我们还发现了另一个非常相似的场景。我有来自拥有该块的线程的堆栈跟踪:

000000c898897560 00007ff8855b7e5d System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].FindEntry(System.__Canon)
000000c8988975d0 00007ff8855b7d34 System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].TryGetValue(System.__Canon, System.__Canon ByRef)
000000c898897610 00007ff88f6152b3 GP.Components.Extensions.AppDomains.RemotingRunner.CurrentDomain_AssemblyResolve(System.Object, System.ResolveEventArgs)
000000c8988978a0 00007ff886f7276c System.AppDomain.OnAssemblyResolveEvent(System.Reflection.RuntimeAssembly, System.String)
000000c898897bd0 00007ff8e4b2a7f3 [GCFrame: 000000c898897bd0] 
000000c898899b78 00007ff8e4b2a7f3 [HelperMethodFrame_PROTECTOBJ: 000000c898899b78] System.Reflection.RuntimeAssembly._nLoad(System.Reflection.AssemblyName, System.String, System.Security.Policy.Evidence, System.Reflection.RuntimeAssembly, System.Threading.StackCrawlMark ByRef, IntPtr, Boolean, Boolean, Boolean)
000000c898899c80 00007ff886f7224e System.Reflection.RuntimeAssembly.InternalGetSatelliteAssembly(System.String, System.Globalization.CultureInfo, System.Version, Boolean, System.Threading.StackCrawlMark ByRef)
000000c898899d60 00007ff886f716c8 System.Resources.ManifestBasedResourceGroveler.GetSatelliteAssembly(System.Globalization.CultureInfo, System.Threading.StackCrawlMark ByRef)
000000c898899df0 00007ff885b932fb System.Resources.ManifestBasedResourceGroveler.GrovelForResourceSet(System.Globalization.CultureInfo, System.Collections.Generic.Dictionary`2, Boolean, Boolean, System.Threading.StackCrawlMark ByRef)
000000c898899eb0 00007ff885b92ecb System.Resources.ResourceManager.InternalGetResourceSet(System.Globalization.CultureInfo, Boolean, Boolean, System.Threading.StackCrawlMark ByRef)
000000c898899fa0 00007ff885b92b73 System.Resources.ResourceManager.InternalGetResourceSet(System.Globalization.CultureInfo, Boolean, Boolean)
000000c898899ff0 00007ff885b92014 System.Resources.ResourceManager.GetString(System.String, System.Globalization.CultureInfo)
000000c89889a0a0 00007ff89914aa62 NewRelic.Agent.Core.Config.ConfigurationLoader.InitializeFromXml(System.String, System.String)
000000c89889a140 00007ff89914a838 NewRelic.Agent.Core.Config.ConfigurationLoader.Initialize(System.String)
000000c89889a1a0 00007ff899143be9 NewRelic.Agent.Core.Config.ConfigurationLoader.Initialize()
000000c89889a210 00007ff899123a27 NewRelic.Agent.Core.Agent+AgentSingleton.CreateInstance()
000000c89889a280 00007ff8991239c2 NewRelic.Agent.Core.Singleton`1[[System.__Canon, mscorlib]]..ctor(System.__Canon)
000000c89889a2c0 00007ff89912388b NewRelic.Agent.Core.Agent..cctor()
000000c89889a700 00007ff8e4b2a7f3 [GCFrame: 000000c89889a700] 
000000c89889ce88 00007ff8e4b2a7f3 [PrestubMethodFrame: 000000c89889ce88] NewRelic.Agent.Core.Agent.get_Instance()
000000c89889cef0 00007ff89912358c NewRelic.Agent.Core.AgentShim.GetTracer(System.String, UInt32, System.String, System.String, System.Type, System.String, System.String, System.String, System.Object, System.Object[])
000000c89889d280 00007ff8e4b2a7f3 [DebuggerU2MCatchHandlerFrame: 000000c89889d280]

它不是关于TimeZoneHelper类,但有趣的是有一个共同的方面:两个类都在其静态构造函数中加载资源(NewRelic的配置文件或带有TimeZones的文件)​​。因此情况似乎如下:

  1. 多个线程尝试使用类
  2. 第一个线程获取静态构造函数的锁并运行此构造函数
  3. 加载资源,.NET运行时尝试加载资源程序集。
  4. 我们捕获AssemblyResolve事件来加载资源程序集并以某种方式导致死锁,问题是如何?

1 个答案:

答案 0 :(得分:1)

以下是我对发生的事情的猜测。

更新: 我认为这是AssemblyResolve事件的递归问题。 基于注释,没有发生堆栈溢出,但仍然可能存在递归问题,因此答案仍然适用。

有迹象表明此错误取决于访问资源的顺序。很可能这种情况发生在第一件事是访问你提到的静态类之一时。

第一次访问资源时,AssemblyResolve事件会多次触发。后续资源请求不会导致AssemblyResolve事件。这可以通过以下代码来证明:

AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) =>
{
    Console.WriteLine("Resolve {0}", eventArgs.Name);
    return null;
};
Console.WriteLine(Resource1.String1);
Console.WriteLine(Resource1.String1);

结果:

Resolve ConsoleApplication1.resources, Version=1.0.0.0, Culture=ru-RU, PublicKeyToken=null
Resolve ConsoleApplication1.resources, Version=1.0.0.0, Culture=ru-RU, PublicKeyToken=null
Resolve ConsoleApplication1.resources, Version=1.0.0.0, Culture=ru, PublicKeyToken=null
Resolve ConsoleApplication1.resources, Version=1.0.0.0, Culture=ru, PublicKeyToken=null
Value from resource
Value from resource

记录器正在访问资源,这表示为:

000000c898899ff0 00007ff885b92014 System.Resources.ResourceManager.GetString(System.String, System.Globalization.CultureInfo)
000000c89889a0a0 00007ff89914aa62 NewRelic.Agent.Core.Config.ConfigurationLoader.InitializeFromXml(System.String, System.String)
000000c89889a140 00007ff89914a838 NewRelic.Agent.Core.Config.ConfigurationLoader.Initialize(System.String)
000000c89889a1a0 00007ff899143be9 NewRelic.Agent.Core.Config.ConfigurationLoader.Initialize()
000000c89889a210 00007ff899123a27 NewRelic.Agent.Core.Agent+AgentSingleton.CreateInstance()
000000c89889a280 00007ff8991239c2 NewRelic.Agent.Core.Singleton`1[[System.__Canon, mscorlib]]..ctor(System.__Canon)
000000c89889a2c0 00007ff89912388b NewRelic.Agent.Core.Agent..cctor()
000000c89889a700 00007ff8e4b2a7f3 [GCFrame: 000000c89889a700] 
000000c89889ce88 00007ff8e4b2a7f3 [PrestubMethodFrame: 000000c89889ce88] NewRelic.Agent.Core.Agent.get_Instance()
000000c89889cef0 00007ff89912358c NewRelic.Agent.Core.AgentShim.GetTracer(System.String, UInt32, System.String, System.String, System.Type, System.String, System.String, System.String, System.Object, System.Object[])

我的结论是,记录器可以在没有AssemblyResolve的情况下成功运行任何第一次绑定的事件,并且如果第一次以这种方式运行,则不会导致AssemblyResolve事件。

如果您是第一次从AssemblyResolve访问资源,则会发生递归调用,从而导致StackOverflowException。这很容易建模:

AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) =>
{
    Console.WriteLine("Resolve {0}", eventArgs.Name);
    Console.WriteLine(Resource1.String1);
    return null;
};

Console.WriteLine(Resource1.String1);

有一个对Logger的调用:

catch
{
    context.RunnerLog.Error(string.Format(CultureInfo.InvariantCulture, "Failed to load assembly {0}.", args.Name));

    result = null;
}

如果在绑定AssemblyResolve事件之前初始化了logger,或者存在另一个不会导致记录器触发失败的AssemblyResolve事件的情况,则可能存在差异。

当您开始调用静态类并在AssemblyResolve中有异常,并且您应该捕获并记录它时,对logger的调用会导致对资源的访问,而这会导致另一个程序集解析递归会导致堆栈溢出。

当第一个请求对静态类构造函数有锁定时,如果该操作在StackOverflowException之前持续了很长时间,则其他请求被阻止,但这并不重要,因为它们会因TypeInitializationException而失败。后者永远不会发生,因为无论如何域都会在StackOverflowException之后开始卸载。

它在顶部显示一些字典Find方法的事实也无关紧要 - 它可能是导致堆栈溢出的最后一滴。

我建议在AssemblyResolve事件处理程序中使用另一种记录器。

另一件事是我会尝试避免静态构造函数中的任何阻塞IO请求,例如资源访问或手动程序集加载。只需初始化内部的基本内容,并在公共方法本身中使用另一种并发机制进行延迟初始化。

但是,我不认为可疑stackoverflow的原因与静态构造函数有关。

此外,如果递归过慢导致stackoverflow发生,则可能没有可疑的堆栈溢出。这样,域可以通过其他原因开始卸载 - 例如,通过IIS的某些资源消耗保护,例如线程数量或一般内存消耗。如果请求长时间阻止,可能会发生这种情况。