【问题标题】:How can I avoid this deadlock in DevForce when using interceptors使用拦截器时如何避免 DevForce 中的这种死锁
【发布时间】:2017-10-18 16:03:07
【问题描述】:

我们最近开始采用严重依赖 DevForce 的业务逻辑,并通过 Web API 公开它。我们通过确保每个请求都有自己的一组实体、自己的EntityManager 等来非常小心地避免线程问题。但是,我们已经开始注意到逻辑死锁(在 .net 代码中,而不是在 SQL 中)。是很多并发请求。

我已经找到了由PropertyInterceptors 完成的锁定问题。我们非常广泛地使用它们,并且在某些情况下,一个属性(属性 A)上的拦截器将设置另一个属性(属性 B),但反过来也是如此(设置 B 也将设置 A)。其中一些情况的确切原因很复杂,但基本思想是我们想要保持同步的一些属性。 PropertyInterceptor 逻辑内部似乎存在锁定,因此我们很容易遇到死锁,因为获取这些锁定的顺序可能会有所不同。

我在下面创建了一个简单的可重现案例,其中涉及一个只有两个属性的实体。一个是整数属性,另一个是字符串属性。我有BeforeSet 逻辑来使这两者保持同步。在一次设置一个属性的简单情况下,一切正常。但是由于我们正在处理一个 web api,所以并行执行的事情是很常见的。如果我们收到一个碰巧设置了IntValue 的请求,而另一个碰巧设置了StringValue 的请求,我们就会遇到死锁。即使我们在两个不同的EntityManagers 中讨论两个不同的实体也是如此。从我们的角度来看,我们正在以线程安全的方式做所有事情,但是 DevForce 有一些我们知道可能很危险的长期存在的锁。

这是希望解释事情的代码。请记住,我们的实际代码要复杂得多,但基本的死锁是相同的:

public static void ReproduceDeadlock()
{
    var e1 = new MyEntity();
    var e2 = new MyEntity();

    //This works - settings fields one at a time is fine
    e1.IntValue = 1;
    e2.StringValue = "2";

    //But if we introduce some concurrency, we'll become deadlocked
    Task.Run(() =>
    {
        //Wait a bit so e1.IntValue has a chance to start
        Thread.Sleep(1000);

        e2.StringValue = "22";
    });

    e1.IntValue = 11;

    //Execution will never make it hear...setting the IntValue will never complete
}

public class MyEntity : Entity
{
    [BeforeSet("StringValue")]
    public void BeforeSetStringValue(PropertyInterceptorArgs<MyEntity, string> args)
    {
        //When the string is set, 'sync' it to the IntValue property
        IntValue = int.Parse(args.Value);
    }

    [BeforeSet("IntValue")]
    public void BeforeSetIntValue(PropertyInterceptorArgs<MyEntity, int> args)
    {
        //When the int is set, 'sync' it to the StringValue property

        //Introduce a delay so the deadlock will obviously happen.  In our real app, we don't have
        //  a Thread.Sleep() but we do have non-trivial logic that can cause just enough delay for the deadlock
        //  to happen sometimes
        Thread.Sleep(2000);
        StringValue = args.Value.ToString();
    }

    #region PropertyMetadata stuff

    public class PropertyMetadata
    {
        public static readonly DataEntityProperty<MyEntity, string> StringValue =
            new DataEntityProperty<MyEntity, string>("StringValue", true, false,
                ConcurrencyStrategy.None, false, null,
                false);

        public static readonly DataEntityProperty<MyEntity, int> IntValue =
            new DataEntityProperty<MyEntity, int>("IntValue", true, false,
                ConcurrencyStrategy.None, false, null,
                false);
    }

    public string StringValue
    {
        get { return PropertyMetadata.StringValue.GetValue(this); }
        set { PropertyMetadata.StringValue.SetValue(this, value); }
    }

    public int IntValue
    {
        get { return PropertyMetadata.IntValue.GetValue(this); }
        set { PropertyMetadata.IntValue.SetValue(this, value); }
    }

    #endregion
}

}

【问题讨论】:

  • 我要过几天才能调查此事。 PropertyInterceptorManager 是一个单例,因此属性拦截器不是 EntityManager 本地的。每个 PropertyInterceptor 实例在执行时都会锁定线程安全,但我想知道这里发生的事情是否发生在 PropertyInterceptorManager 的发现/初始化阶段。这通常是懒惰地发生,但您可以通过调用 PropertyInterceptorManager.CurrentInstance.DiscoverInterceptorsFromAttributes() 来强制它。
  • 好的,正如您所发现的,死锁确实发生在执行拦截器操作时,而不是在发现/初始化时。看起来我们需要在 PI 执行期间使用线程局部变量,而不是锁定。
  • 那太好了。 Thread-local 似乎是一个很好的解决方案。我猜我现在真的没有可以使用的解决方法吗?我尝试了一些东西,但似乎没有任何效果。您是否粗略估计了该修复程序何时可用?
  • PropertyInterceptor 类中的线程局部变量存在性能问题,因此解决方案可能不是那么简单。该修复程序可能会在 6 月中旬发布,但如果您需要它,请尽快告诉我,因为我也想不出任何解决方法。
  • 只是检查修复是否仍按计划在 6 月中旬进行?您建议的解决方法对我们不起作用,所以我们希望尽快得到完整的修复...... :)

标签: c# multithreading deadlock devforce


【解决方案1】:

斯蒂芬,也许我有一个解决方法给你。在拦截器操作中,您可以使用 SetValueRaw 将值同步到另一个属性并避免通过其拦截器(和验证)。该方法在公共 IStructuralObject 接口上可用,虽然记录为仅供内部使用,但我们不打算更改。 EntityAspect 和 ComplexAspect 类都实现了这个接口。

所以你的例子应该是这样的:

[BeforeSet("StringValue")]
public void BeforeSetStringValue(PropertyInterceptorArgs<MyEntity, string> args)
{
    //When the string is set, 'sync' it to the IntValue property
    (this.EntityAspect as IStructuralObject).SetValueRaw(PropertyMetadata.IntValue, int.Parse(args.Value));
}

[BeforeSet("IntValue")]
public void BeforeSetIntValue(PropertyInterceptorArgs<MyEntity, int> args)
{
    //When the int is set, 'sync' it to the StringValue property

    //Introduce a delay so the deadlock will obviously happen.  In our real app, we don't have
    //  a Thread.Sleep() but we do have non-trivial logic that can cause just enough delay for the deadlock
    //  to happen sometimes
    Thread.Sleep(2000);

    (this.EntityAspect as IStructuralObject).SetValueRaw(PropertyMetadata.StringValue, args.Value.ToString());
}

我还会指出另一种解决方法。死锁在拦截器中,但您可以让所有常见的验证逻辑和更改通知发生,只是没有拦截层。一种方法是将 EntityGroup.PropertyInterceptionEnabled 标志设置为 false,但打开和关闭它通常很笨拙。另一种选择是一个辅助函数来完成 SetterInterceptor 正在做的事情:

public static void SetValueWithVerification(IStructuralObject so, DataEntityProperty property, object newValue)
{
    if (so.VerifierEngine != null && so.VerifierEngine.Enabled && so.EntityGroup.VerificationEnabled)
    {
        if ((property.MemberMetadata.VerifierSetterOptions & VerifierSetterOptions.BeforeSet) > 0)
        {
            so.ValidatePropertyBeforeSet(property, newValue);
        }

        so.SetValueWithChangeNotification(property, newValue);

        if ((property.MemberMetadata.VerifierSetterOptions & VerifierSetterOptions.AfterSet) > 0)
        {
            so.ValidatePropertyAfterSet(property, newValue);
        }
    }
    else
    {
        so.SetValueWithChangeNotification(property, newValue);
    }
}

然后在这些紧密耦合的拦截器动作中调用它:

SetValueWithVerification(this.EntityAspect, PropertyMetadata.StringValue, args.Value.ToString());

【讨论】:

  • 感谢您的更新。乍一看,我认为这不会有帮助。我们可能确实想要拦截器/验证/等。作为同步的一部分仍然触发。如果在IntValue 上进行了验证并且有人设置了StringValue,我仍然希望出现验证错误。此外,我们在 OnPropertyChanged 中有很多逻辑,我仍然希望它能够触发。但我会看看这个......也许我可以以某种方式使用它......
  • 我已经测试了这个更新的解决方法,它似乎可以解决我们一直遇到的一个死锁。这在短期内对我们有用,但似乎不是一个好的长期解决方案。这个死锁花了很多时间来解决和实际识别。如果我们在生产中看到另一个僵局,那将是非常痛苦的。此外,我们有数百个具有大量此类逻辑的实体,我们无法急切地重写所有这些以使用此解决方法。
  • 好的,感谢您试用。我们仍按计划在下周初为您提供预发布。
  • 这是个好消息。我一直在测试这个变通方法,但我没有昨天那么兴奋。解决方法显然跳过了拦截器操作,但我意识到我们在所有地方(以及我忘记的地方)都使用了这些操作......所以现在,我只等到下周才能真正修复。谢谢!
猜你喜欢
  • 1970-01-01
  • 2015-04-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-04
  • 1970-01-01
  • 2013-06-07
  • 2012-08-14
相关资源
最近更新 更多