在锁的获取顺序不确定时避免死锁问题

3

假设我有以下类:

public class IntBagWithLock
{
    private readonly lockObject = new object();
    private bool assigned = false;
    private int data1;
    private int data2;

    public int? Data1
    {
        get { lock (lockObject) { return assigned ? data1 : (int?)null; } }
    }
    public int? Data2
    {
        get { lock (lockObject) { return assigned ? data2 : (int?)null; } }
    }
    public bool Assigned { get { lock(lockObject) { return assigned; } }

    public bool TrySetData(int value1, int value2)
    {
        lock (lockObject)
        {
            if (assigned) return false;

            data1 = value1;
            data2 = value2;
            assigned = true;
            return true;
        }
    }

    public bool IsEquivalentTo(IntBagWithLock other)
    {
        if (ReferenceEquals(this, other)) return true;
        if (ReferenceEquals(other, null)) return false;

        lock (lockObject)
        {
            if (!assigned) return false;
            lock (other.lockObject)
            {
                return other.assigned && other.data1 == data1 && other.data2 == data2;
            }
        }
    }
}

我担心的问题是,由于IsEquivalentTo的实现方式,如果一个线程调用item1.IsEquivalentTo(item2)并获取了item1的锁,另一个线程调用item2.IsEquivalentTo(item1)并获取了item2的锁,就会导致死锁的情况。
为了尽可能避免这种死锁的发生,应该怎么做呢? 更新2:代码示例已经修改成更接近我的实际情况了。我认为所有的答案仍然有效。

有些地方让我感觉不对劲,能否告诉我们您的实际应用场景? - Mitch Wheat
@MitchWheat:我有一个类,它包含两个数据字段(这两个字段都是不可变的值),一个实例可以从一个状态转换到另一个状态,并且我希望尽可能多地在已知状态下操作。 - Jean Hominal
你能否让IntBagWithLock只能通过构造函数进行初始化?这样就可以摆脱锁定。 - Sriram Sakthivel
@SriramSakthivel:是的,不可变性通常是我用来确保线程安全的方法,但在这种情况下,我相当确定我希望数据在实例化后被修改。 - Jean Hominal
你有不可变的字段可以更改吗?我对这种不可变性的特定定义不熟悉。 - Jim Mischel
显示剩余3条评论
4个回答

2
通常情况下,您需要为每个对象分配一个唯一的ID,并且从较低的ID到较高的ID进行锁定。
public class BagWithLock
{
    // The first Id generated will be 1. If you want it to be 0, put
    // here -1 .
    private static int masterId = 0; 

    private readonly object locker = new object();

    private readonly int id = Interlocked.Increment(ref masterId);

    public static void Lock(BagWithLock bwl1, BagWithLock bwl2, Action action)
    {
        if (bwl1.id == bwl2.id)
        {
            // same object case
            lock (bwl1.locker)
            {
                action();
            }
        }
        else if (bwl1.id < bwl2.id)
        {
            lock (bwl1.locker)
            {
                lock (bwl2.locker)
                {
                    action();
                }
            }
        }
        else
        {
            lock (bwl2.locker)
            {
                lock (bwl1.locker)
                {
                    action();
                }
            }
        }
    }
}

您可以这样使用它:
bool equals;

BagWithLock(bag1, bag2, () => {
    equals = bag1.SequenceEquals(bag2);
});

所以你需要传递一个包含在lock中执行的操作的Action

Interlocked.Increment对于static masterId可以保证每个类都有唯一的id。请注意,如果您创建了超过40亿个该类的实例,将会出现问题。如果需要,请使用long


即使它环绕着你,你仍然可以排序;比较两个相等的数字的机会是极其小的(但同时也是真实的)。 - Marc Gravell
@MarcGravell 这是一种经典的 bug,你可能需要搜寻数天才能找到它 :-) 至少我会加上这样一个检查: if (bwl1.id == bwl2.id) { if (!object.ReferenceEquals) { throw Exception("Equal ID!"); } - xanatos

1
我不知道为什么你每次使用get或equal时都要锁定,但你可以这样做:
public bool IsEquivalentTo(BagWithLock other)
{
    object myData;
    object otherData;
    lock (lockObject)
        myData = data;

    lock (other.lockObject)
        otherData = other.data;

    return object.Equals(myData, otherData);
}

那样物品在比较时不会改变。
总的来说,这种锁有一些缺点,我认为我会做一个静态的 lockObject,这样你只能在一个方法中拥有一个对象,避免竞争条件。

更新 根据您的更新,我建议您使用:

private static readonly object equalLock = new object();

public bool IsEquivalentTo(IntBagWithLock other)
{
    lock(equalLock){
       if (ReferenceEquals(this, other)) return true;
       if (ReferenceEquals(other, null)) return false;
         if (!assigned) return false;
           return other.assigned && other.data1 == data1 && other.data2 == data2;
   }
}

1
应该复制对象(如果可能的话),而不是引用它。否则,在比较期间,它将无法防止对象被修改。 - Medinoc
@JeanHominal 因为我可以使用 get 获取该对象,随时持有并更改它,而无需锁定以阻止我获取。 - No Idea For Name
@JeanHominal 在这种情况下,一次锁定互斥量或其他几种方式都可以完美地工作,您应该在 getter 中释放锁定。 - No Idea For Name
@NoIdeaForName:是的,这让我觉得我把示例代码简化得太多了——实际上我在getter和setter中检查了另一个数据字段。我会发布另一个问题。 - Jean Hominal
@JeanHominal 并不总是这样,这取决于情况。例如,如果您告诉我您每秒要执行10000次比较,我会建议您不要使用共享锁,而是使用GetHashCode并通过从低到高的数字锁定对象,以获得统一的行为。 - No Idea For Name
显示剩余6条评论

1

由于OP提到dataImmutable,我认为这里根本不需要锁定,`volatile'应该就可以解决问题。

public class BagWithLock
{
    private volatile object data;
    public object Data
    {
        get { lock return data; }
        set { data = value;  }
    }
    public bool IsEquivalentTo(BagWithLock other)
    {
        return object.Equals(data, other.data);
    }
}

这应该是线程安全的。如果我错误,请纠正我。


我已经更新了问题,以更好地解释为什么我想要锁定 - 也就是说,在getter内部有一个更复杂的逻辑,检查另一个字段。 - Jean Hominal

0
也许你可以使用更昂贵的基于WaitHandle的锁定对象(例如Mutex),并在需要多个锁时使用WaitHandle.WaitAll()

使用WaitHandle.WaitAll来获取多个锁可能会导致死锁。两个线程可能会等待同一组锁。每个线程都获得其中一个锁,并继续等待另一个……情况很快就会变得混乱。 - Jim Mischel
我认为WaitAll(以及底层的Win32函数WaitForMultipleObjects)可以通过“原子”地获取或释放所有锁来专门处理此情况,就像nx中的semop一样... - Medinoc
1
你说得对。WaitForMultipleObjects会等待互斥体可用,但在所有互斥体都可用之前不会获取它们。是我搞错了。 - Jim Mischel

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