多线程线程安全读/写锁定的最佳C#解决方案?

时间:2009-08-25 19:42:48

标签: c# .net multithreading

在C#的多线程环境中,对static成员锁定读/写访问权限的最安全(和最短)方法是什么?

是否可以进行线程安全锁定&在类级解锁(所以每次需要静态成员访问时我都不会重复锁定/解锁代码)?

修改:示例代码非常棒:)

修改:我应该使用 volatile 关键字还是 Thread.MemoryBarrier()来避免多处理器缓存,还是不必要?据Jon Skeet说,只有那些会让其他处理器看到变化吗? (单独询问here)。

8 个答案:

答案 0 :(得分:10)

小值

对于小值(基本上任何可以声明为volatile的字段),您可以执行以下操作:

private static volatile int backingField;

public static int Field
{
    get { return backingField; }
    set { backingField = value; }
} 

大值

如果值大于32位计算机上的32位或64位计算机上的64位,则赋值较大时,赋值将不是原子。请参阅ECMA 335 12.6.6规范。因此,对于引用类型和大多数内置值类型,赋值是原子的,但是如果你有一些大的结构,比如:

struct BigStruct 
{
    public long value1, valuea0a, valuea0b, valuea0c, valuea0d, valuea0e;
    public long value2, valuea0f, valuea0g, valuea0h, valuea0i, valuea0j;
    public long value3;
}

在这种情况下,您需要在get访问器周围进行某种锁定。您可以使用ReaderWriterLockSlim进行此操作,我将在下面演示。 Joe Duffy使用ReaderWriterLockSlim vs ReaderWriterLock时已advice

    private static BigStruct notSafeField;
    private static readonly ReaderWriterLockSlim slimLock = 
        new ReaderWriterLockSlim();

    public static BigStruct Safe
    {
        get
        {
            slimLock.EnterReadLock();
            var returnValue = notSafeField;
            slimLock.ExitReadLock();

            return returnValue;
        }
        set
        {
            slimLock.EnterWriteLock();
            notSafeField = value;
            slimLock.ExitWriteLock();
        }
    }

不安全的Get-Accessor演示

这是我用来表示在get-accessor中没有使用锁时缺乏原子性的代码:

    private static readonly object mutexLock = new object();
    private static BigStruct notSafeField;

    public static BigStruct NotSafe
    {
        get
        {
            // this operation is not atomic and not safe
            return notSafeField;
        }
        set
        {
            lock (mutexLock)
            {
                notSafeField = value;
            }
        }
    }

    public static void Main(string[] args)
    {
        var t = new Thread(() =>
            {
                while (true)
                {
                    var current = NotSafe;
                    if (current.value2 != (current.value1 * 2)
                        || current.value3 != (current.value1 * 5))
                    {
                        throw new Exception(String.Format("{0},{1},{2}", current.value1, current.value2, current.value3));
                    }
                }
            });
        t.Start();
        for(int i=0; i<50; ++i)
        {
            var w = new Thread((state) =>
                {
                    while(true)
                    {
                        var index = (int) state;
                        var newvalue = new BigStruct();
                        newvalue.value1 = index;
                        newvalue.value2 = index * 2;
                        newvalue.value3 = index * 5;
                        NotSafe = newvalue;
                    }
                });
            w.Start(i);
        }
        Console.ReadLine();
    }

答案 1 :(得分:3)

最安全和最短的方法是创建一个Object类型的私有静态字段,仅用于锁定(将其视为“pad-lock”对象)。使用此字段并仅锁定此字段,因为这会阻止其他类型锁定代码,然后锁定您所使用的相同类型。

如果您锁定类型本身,则存在另一种类型也会决定锁定您的类型的风险,这可能会造成死锁。

以下是一个例子:

class Test
{
    static readonly Object fooLock = new Object();
    static String foo;

    public static String Foo
    {
        get { return foo; }
        set
        {
            lock (fooLock)
            {
                foo = value;
            }
        }
    }
}

请注意,我已经创建了一个用于锁定foo的私有静态字段 - 我使用该字段来锁定该字段上的写操作。

答案 2 :(得分:3)

虽然您可以使用单个互斥锁来控制对类的所有访问(有效地序列化对类的访问),但我建议您研究静态类,确定哪些成员正在使用的位置和方式以及使用哪一个或者几个ReaderWriterLock(MSDN文档中的代码示例),它提供了对多个读者的访问,但同时只能访问一个编写者。

这样你就会得到一个细粒度的多线程类,它只会阻止写入,但会同时允许多个读者,并允许在读取另一个不相关的成员时写入一个成员。

答案 3 :(得分:2)

class LockExample {
    static object lockObject = new object();
    static int _backingField = 17;

    public static void NeedsLocking() {
        lock(lockObject) {
            // threadsafe now
        }
    }

    public static int ReadWritePropertyThatNeedsLocking {
        get {
            lock(lockObject) {
                // threadsafe now
                return _backingField;
            }
        }
        set {
            lock(lockObject) {
                // threadsafe now
                _backingField = value;
            }
        }
    }
}

lock关于专门为此目的而不是typeof(LockExample)创建的对象,以防止其他人锁定LockExample类型对象的死锁情况。

  

是否可以进行线程安全锁定&amp;在类级解锁(所以每次需要静态成员访问时我都不会重复锁定/解锁代码)?

仅在您需要的地方lock,并在被调用者内部进行,而不是要求调用者执行lock

答案 4 :(得分:2)

其他几个人已经解释了如何将lock关键字与私有锁对象一起使用,所以我只想添加:

请注意,即使您锁定了类型中的每个方法,也不能将序列中的多个方法调用为原子方法。例如,如果您正在实现字典并且您的接口具有Contains方法和Add方法,则调用Contains后跟Add将 not 为原子。有人可以修改包含和添加的调用之间的字典 - 即存在竞争条件。要解决此问题,您必须更改接口并提供类似AddIfNotPresent(或类似)的方法,该方法将检查和修改封装为单个操作。

Jared Par有一个很好的blog post on the topic(一定要阅读评论)。

答案 5 :(得分:1)

应该根据需要在静态访问者的每个静态成员访问中锁定/解锁。

保留一个私有对象用于锁定,并根据需要锁定。这使锁定尽可能细,这非常重要。它还使锁定内部保持静态类成员。如果你锁定在班级,你的调用者将负责锁定,这会损害可用性。

答案 6 :(得分:0)

我感谢大家,我很高兴分享这个演示程序,受上述贡献的启发,运行3种模式(不安全,互斥,超薄)。

请注意,设置“Silent = false”将导致线程之间完全没有冲突。使用此“Silent = false”选项可以在控制台中写入所有线程。

UPDATE sk_users set dst_date = date(dst_date);

答案 7 :(得分:-2)

锁定静态方法听起来是个坏主意,但有一点如果你从类构造函数中使用这些静态方法,你可能会遇到一些有趣的副作用,因为加载器锁(以及类加载器可以忽略其他锁的事实)