Unity可以不断抛出SynchronizationLockException吗?

65
Unity依赖注入容器存在一个似乎是广为人知的问题,即SynchronizedLifetimeManager经常会导致Monitor.Exit方法抛出SynchronizationLockException异常,该异常随后被捕获并忽略。这对我造成了麻烦,因为我喜欢使用Visual Studio调试工具,在任何异常抛出时停止调试,因此每次我的应用程序启动时,我都会无缘无故地多次遇到这个异常。
如何防止这个异常被抛出?
在其他网站上提到这个问题时,通常的建议是更改调试器设置以忽略它。这就像去看医生说:“医生,医生,我举起手臂时手臂会疼”,然后被告知:“那你别举起它。”我正在寻找一种解决方案,可以在第一时间防止异常被抛出。
异常发生在SetValue方法中,因为它假设GetValue将首先被调用,其中调用了Monitor.Enter。但是,LifetimeStrategy和UnityDefaultBehaviorExtension类经常在不调用GetValue的情况下调用SetValue。
我不想改变源代码并维护自己的Unity版本,因此我希望有一种解决方案,可以向容器添加一些扩展、策略或规则的组合,以确保如果生命周期管理器是SynchronizedLifetimeManager,那么在任何其他操作之前总是会调用GetValue。
8个回答

38

我相信代码中有很多种方式调用SynchronizedLifetimeManager,或者像ContainerControlledLifetimeManager这样的子类,但有两种情况特别引起了我的问题。

第一个问题是我的错 - 我正在使用构造函数注入来提供对容器的引用,并且在那个构造函数中我还将该类的新实例添加到容器中以备将来使用。这种反向的方法会改变生命周期管理器从Transient到ContainerControlled,导致Unity在GetValue上调用的对象与SetValue上调用的对象不同。得出的教训是不要在构建期间做任何可能改变对象生命周期管理器的事情。

第二种情况是每次调用RegisterInstance时,UnityDefaultBehaviorExtension都会在没有先调用GetValue的情况下调用SetValue。幸运的是,Unity具有足够的可扩展性,只要努力一些,就可以解决这个问题。

从这里开始介绍一个新的行为扩展,如下所示:

/// <summary>
/// Replaces <see cref="UnityDefaultBehaviorExtension"/> to eliminate 
/// <see cref="SynchronizationLockException"/> exceptions that would otherwise occur
/// when using <c>RegisterInstance</c>.
/// </summary>
public class UnitySafeBehaviorExtension : UnityDefaultBehaviorExtension
{
    /// <summary>
    /// Adds this extension's behavior to the container.
    /// </summary>
    protected override void Initialize()
    {
        Context.RegisteringInstance += PreRegisteringInstance;

        base.Initialize();
    }

    /// <summary>
    /// Handles the <see cref="ExtensionContext.RegisteringInstance"/> event by
    /// ensuring that, if the lifetime manager is a 
    /// <see cref="SynchronizedLifetimeManager"/> that its 
    /// <see cref="SynchronizedLifetimeManager.GetValue"/> method has been called.
    /// </summary>
    /// <param name="sender">The object responsible for raising the event.</param>
    /// <param name="e">A <see cref="RegisterInstanceEventArgs"/> containing the
    /// event's data.</param>
    private void PreRegisteringInstance(object sender, RegisterInstanceEventArgs e)
    {
        if (e.LifetimeManager is SynchronizedLifetimeManager)
        {
            e.LifetimeManager.GetValue();
        }
    }
}

那么您需要一种替换默认行为的方法。Unity没有删除特定扩展名的方法,因此您需要将所有内容删除,然后再重新添加其他扩展名:

public static IUnityContainer InstallCoreExtensions(this IUnityContainer container)
{
    container.RemoveAllExtensions();
    container.AddExtension(new UnityClearBuildPlanStrategies());
    container.AddExtension(new UnitySafeBehaviorExtension());

#pragma warning disable 612,618 // Marked as obsolete, but Unity still uses it internally.
    container.AddExtension(new InjectedMembers());
#pragma warning restore 612,618

    container.AddExtension(new UnityDefaultStrategiesExtension());

    return container;
}

注意UnityClearBuildPlanStrategies吗? RemoveAllExtensions会清除容器内的所有策略和策略列表,除了一个之外,因此我必须使用另一个扩展来避免在恢复默认扩展时插入重复项:

/// <summary>
/// Implements a <see cref="UnityContainerExtension"/> that clears the list of 
/// build plan strategies held by the container.
/// </summary>
public class UnityClearBuildPlanStrategies : UnityContainerExtension
{
    protected override void Initialize()
    {
        Context.BuildPlanStrategies.Clear();
    }
}
现在你可以放心地使用RegisterInstance而不必担心被推向疯狂的边缘。只是为了确保,这里有一些测试:
[TestClass]
public class UnitySafeBehaviorExtensionTests : ITest
{
    private IUnityContainer Container;
    private List<Exception> FirstChanceExceptions;

    [TestInitialize]
    public void TestInitialize()
    {
        Container = new UnityContainer();
        FirstChanceExceptions = new List<Exception>();
        AppDomain.CurrentDomain.FirstChanceException += FirstChanceExceptionRaised;
    }

    [TestCleanup]
    public void TestCleanup()
    {
        AppDomain.CurrentDomain.FirstChanceException -= FirstChanceExceptionRaised;
    }

    private void FirstChanceExceptionRaised(object sender, FirstChanceExceptionEventArgs e)
    {
        FirstChanceExceptions.Add(e.Exception);
    }

    /// <summary>
    /// Tests that the default behavior of <c>UnityContainer</c> leads to a <c>SynchronizationLockException</c>
    /// being throw on <c>RegisterInstance</c>.
    /// </summary>
    [TestMethod]
    public void UnityDefaultBehaviorRaisesExceptionOnRegisterInstance()
    {
        Container.RegisterInstance<ITest>(this);

        Assert.AreEqual(1, FirstChanceExceptions.Count);
        Assert.IsInstanceOfType(FirstChanceExceptions[0], typeof(SynchronizationLockException));
    }

    /// <summary>
    /// Tests that <c>UnitySafeBehaviorExtension</c> protects against <c>SynchronizationLockException</c>s being
    /// thrown during calls to <c>RegisterInstance</c>.
    /// </summary>
    [TestMethod]
    public void SafeBehaviorPreventsExceptionOnRegisterInstance()
    {
        Container.RemoveAllExtensions();
        Container.AddExtension(new UnitySafeBehaviorExtension());
        Container.AddExtension(new InjectedMembers());
        Container.AddExtension(new UnityDefaultStrategiesExtension());

        Container.RegisterInstance<ITest>(this);

        Assert.AreEqual(0, FirstChanceExceptions.Count);
    }
}

public interface ITest { }

我们正在使用Enterprise Library Logger,但不是Unity,由于Logger在内部使用Unity,所以我们遇到了相同的问题。我能否在没有Unity的情况下应用该修复程序? - Dmitry Gusarov
@DmitryGusarov 我没有尝试过,但如果你可以获取到EntLib内部使用的UnityContainer实例的引用,那么你应该能够在其上使用InstallCoreExtensions方法。你应该检查EntLib是否添加了任何自己的扩展 - 如果是这样,你需要在InstallCoreExtensions删除所有内容后将它们添加回来。 - Rory MacLeod
4
请勿在Unity的最新版本中使用此解决方案,否则会导致无限循环。 - Softlion


10

很遗憾,对于你的问题,答案是否定的。我在微软模式与实践组(我最近是该组的开发主管)与开发团队进行了跟进,我们将其作为 EntLib 5.0 考虑的一个 bug。我们进行了一些调查,并得出结论,这是由我们的代码和调试器之间的一些意外交互引起的。我们考虑了修复,但结果比现有代码更加复杂。最终,这个问题被优先级降低,没有满足5的标准。

很抱歉我没有更好的答案。如果有任何安慰的话,我也觉得这很烦人。


1
感谢亲口给出的答案!幸运的是,Unity非常可扩展,所以我能够解决这个问题 - 请参阅我的回答 - Rory MacLeod
Ade完美地总结了EntLib团队在开发EntLib5.0时所经历的决策。如果这仍然是许多人的主要烦恼因素,请在我们的uservoice网站上提出建议并投票支持。在规划Unity和EntLib的vNext时,我们将考虑这一点。 - Grigori Melnik

7
我使用这个简短的解决方案:
/// <summary>
/// KVV 20110502
/// Fix for bug in Unity throwing a synchronizedlockexception at each register
/// </summary>
class LifeTimeManager : ContainerControlledLifetimeManager
{
    protected override void SynchronizedSetValue(object newValue)
    {
        base.SynchronizedGetValue();
        base.SynchronizedSetValue(newValue);
    }
}

并像这样使用:

private UnityContainer _container;
...
_container.RegisterInstance(instance, new LifeTimeManager());

问题在于ContainerControlledLifetimeManager的基类期望通过base.GetValue使用SynchronizedSetValue进行monitor.Enter(),然而ContainerControlledLifetimeManager类没有实现这个方法(显然开发人员没有启用“异常中断”功能?)。
祝好, Koen

我该如何将这个修复方案引入应用程序?我不懂...我应该在哪里注册LifeTimeManager?我们没有使用Unity...只有这个Logger... - Dmitry Gusarov

4

Rory的解决方案非常棒 - 谢谢。解决了每天困扰我的问题!我对Rory的解决方案进行了一些微小的调整,以便处理注册的任何扩展名(在我的情况下,我有一个WPF Prism/Composite扩展)。

    public static void ReplaceBehaviourExtensionsWithSafeExtension(IUnityContainer container)
    {
        var extensionsField = container.GetType().GetField("extensions", BindingFlags.Instance | BindingFlags.NonPublic);
        var extensionsList = (List<UnityContainerExtension>)extensionsField.GetValue(container);
        var existingExtensions = extensionsList.ToArray();
        container.RemoveAllExtensions();
        container.AddExtension(new UnitySafeBehaviorExtension());
        foreach (var extension in existingExtensions)
        {
            if (!(extension is UnityDefaultBehaviorExtension))
            {
                container.AddExtension(extension);
            }
        }
    }

请记得清除构建计划策略 - container.AddExtension(new UnityClearBuildPlanStrategies()); - Rory MacLeod

1

注意 Zubin Appoo 的回答中有一个错误:他的代码中缺少了 UnityClearBuildPlanStrategies

正确的代码片段如下:

FieldInfo extensionsField = container.GetType().GetField("extensions", BindingFlags.Instance | BindingFlags.NonPublic);
List<UnityContainerExtension> extensionsList = (List<UnityContainerExtension>)extensionsField.GetValue(container);
UnityContainerExtension[] existingExtensions = extensionsList.ToArray();
container.RemoveAllExtensions();
container.AddExtension(new UnityClearBuildPlanStrategiesExtension());
container.AddExtension(new UnitySafeBehaviorExtension());

foreach (UnityContainerExtension extension in existingExtensions)
{
   if (!(extension is UnityDefaultBehaviorExtension))
   {
       container.AddExtension(extension);
   }
}

1
这段代码片段与Zubin的答案完全相同,不包括调用UnityClearBuildPlanStrategies()。 - Ian Oakes

0

Unity 2.1 - 2012年8月更新修复了该漏洞

  1. 解决线程安全问题: http://unity.codeplex.com/discussions/328841

  2. 改进System.Threading.SynchronizationLockException的调试体验: https://entlib.uservoice.com/forums/89245-general/suggestions/2377307-fix-the-system-threading-synchronizationlockexcep

  3. 通过更好的错误消息提高调试体验,当无法加载类型时: http://unity.codeplex.com/workitem/9223

  4. 支持在没有公共构造函数的类的现有实例上执行BuildUp()的场景: http://unity.codeplex.com/workitem/9460

为了让用户的更新体验尽可能简单,并避免需要进行程序集绑定重定向,我们选择仅增加程序集文件版本,而不是 .NET 程序集版本。

-8
这可能会对你有帮助:
  • 转到调试 -> 异常...
  • 找到真正令你不悦的异常,如SynchronizationLockException

完成。


3
我所说的“建议通常涉及更改调试器设置以忽略它”的意思是,我不想压制任何由我的代码错误引起的SynchronizationLockExceptions异常。请帮我对内容进行翻译并使其更加通俗易懂,但不要改变原本的意思。 - Rory MacLeod

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接