当无法保证锁的获取顺序时,避免死锁

本文关键字:顺序 死锁 获取 | 更新日期: 2023-09-27 18:28:15

假设我有以下类:

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,那么我可能会遇到死锁情况。

我应该怎么做才能尽可能确保这种僵局不会发生?

UPDATE 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);
});

因此您传递一个Action,其中包含您想在lock s中执行的操作。

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

由于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);
    }
}

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

我不知道为什么每次获得或等于时都会锁定,但你可以做到:

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;
   }
}

也许您可以使用成本更高的WaitHandle派生的锁定对象(如Mutex),并在需要同时使用多个锁时使用WaitHandle.WaitAll()