使用拦截器

时间:2017-05-18 17:39:22

标签: c# multithreading deadlock devforce

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

我已将问题跟踪到由PropertyInterceptors完成的锁定。我们非常广泛地使用它们并且有一种情况,其中一个属性上的拦截器(属性A)将设置另一个属性(属性B)但反过来也是如此(设置B也将设置A)。其中一些案例的确切原因很复杂,但基本的想法是我们有一些我们希望保持同步的属性。似乎PropertyInterceptor逻辑内部存在锁定,因此我们可以轻松地查找死锁,因为这些锁定的顺序可能会有所不同。

我在下面创建了一个简单的可重现案例,其中涉及只有两个属性的实体。一个是整数属性,另一个是字符串属性。我有BeforeSet逻辑来保持这两者彼此同步。在一次设置属性的简单情况下,一切正常。但是,由于我们正在处理web api,因此并行执行的事情非常常见。如果我们收到一个设置IntValue的请求和另一个设置StringValue的请求,我们就会遇到死锁。即使我们在两个不同的EntityManager中讨论两个不同的实体,也是如此。从我们的角度来看,我们以线程安全的方式做所有事情但是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
}

}

1 个答案:

答案 0 :(得分: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());