繁体   English   中英

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

[英]Best C# solution for multithreaded threadsafe read/write locking?

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

是否可以在类级别上执行线程安全锁定和解锁(因此,每次需要静态成员访问时,我都不会继续重复锁定/解锁代码)?

编辑 :示例代码会很棒:)

编辑 :我应该使用volatile关键字或Thread.MemoryBarrier()来避免多处理器缓存还是不必要? 据Jon Skeet说,只有那些会让其他处理器看到变化吗? (另被问及这在这里 )。

小价值观

对于小值(基本上任何可以声明为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对使用ReaderWriterLockSlimReaderWriterLock建议

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

最安全和最短的方法是创建一个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的私有静态字段 - 我使用该字段来锁定该字段上的写入操作。

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

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

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的类型对象的死锁情况。

是否可以在类级别上执行线程安全锁定和解锁(因此,每次需要静态成员访问时,我都不会继续重复锁定/解锁代码)?

lock你需要的地方,并在被叫者内部进行,而不是要求呼叫者进行lock

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

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

Jared Par有一篇关于这个主题的优秀博客文章 (请务必阅读评论)。

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

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

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

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Test
{
    class Program
    {
        //------------------------------------------------------------------------------  
        // Configuration.

        const bool Silent = true;

        const int Nb_Reading_Threads = 8;
        const int Nb_Writing_Threads = 8;

        //------------------------------------------------------------------------------  
        // Structured data.

        public class Data_Set
        {
            public const int Size = 20;
            public long[] T;

            public Data_Set(long t)
            {
                T = new long[Size];

                for (int i = 0; i < Size; i++)
                    T[i] = t;
            }

            public Data_Set(Data_Set DS)
            {
                Set(DS);
            }

            public void Set(Data_Set DS)
            {
                T = new long[Size];

                for (int i = 0; i < Size; i++)
                    T[i] = DS.T[i];
            }
        }

        private static Data_Set Data_Sample = new Data_Set(9999);

        //------------------------------------------------------------------------------  
        // SAFE process.

        public enum Mode { Unsafe, Mutex, Slim };
        public static Mode Lock_Mode = Mode.Unsafe;

        private static readonly object Mutex_Lock = new object();
        private static readonly ReaderWriterLockSlim Slim_Lock = new ReaderWriterLockSlim();

        public static Data_Set Safe_Data
        {
            get
            {
                switch (Lock_Mode)
                {
                    case Mode.Mutex:

                        lock (Mutex_Lock)
                        {
                            return new Data_Set(Data_Sample);
                        }

                    case Mode.Slim:

                        Slim_Lock.EnterReadLock();
                        Data_Set DS = new Data_Set(Data_Sample);
                        Slim_Lock.ExitReadLock();

                        return DS;

                    default:

                        return new Data_Set(Data_Sample);
                }
            }
            set
            {
                switch (Lock_Mode)
                {
                    case Mode.Mutex:

                        lock (Mutex_Lock)
                        {
                            Data_Sample.Set(value);
                        }
                        break;

                    case Mode.Slim:

                        Slim_Lock.EnterWriteLock();
                        Data_Sample.Set(value);
                        Slim_Lock.ExitWriteLock();
                        break;

                    default:

                        Data_Sample.Set(value);
                        break;
                }
            }
        }

        //------------------------------------------------------------------------------  
        // Main function.

        static void Main(string[] args)
        {
            // Console.
            const int Columns = 120;
            const int Lines = (Silent ? 50 : 500);

            Console.SetBufferSize(Columns, Lines);
            Console.SetWindowSize(Columns, 40);

            // Threads.
            const int Nb_Threads = Nb_Reading_Threads + Nb_Writing_Threads;
            const int Max = (Silent ? 50000 : (Columns * (Lines - 5 - (3 * Nb_Threads))) / Nb_Threads);

            while (true)
            {
                // Console.
                Console.Clear();
                Console.WriteLine("");

                switch (Lock_Mode)
                {
                    case Mode.Mutex:

                        Console.WriteLine("---------- Mutex ----------");
                        break;

                    case Mode.Slim:

                        Console.WriteLine("---------- Slim ----------");
                        break;

                    default:

                        Console.WriteLine("---------- Unsafe ----------");
                        break;
                }

                Console.WriteLine("");
                Console.WriteLine(Nb_Reading_Threads + " reading threads + " + Nb_Writing_Threads + " writing threads");
                Console.WriteLine("");

                // Flags to monitor all threads.
                bool[] Completed = new bool[Nb_Threads];

                for (int i = 0; i < Nb_Threads; i++)
                    Completed[i] = false;

                // Threads that change the values.
                for (int W = 0; W < Nb_Writing_Threads; W++)
                {
                    var Writing_Thread = new Thread((state) =>
                    {
                        int t = (int)state;
                        int u = t % 10;

                        Data_Set DS = new Data_Set(t + 1);

                        try
                        {
                            for (int k = 0; k < Max; k++)
                            {
                                Safe_Data = DS;

                                if (!Silent) Console.Write(u);
                            }
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("\r\n" + "Writing thread " + (t + 1) + " / " + ex.Message + "\r\n");
                        }

                        Completed[Nb_Reading_Threads + t] = true;
                    });

                    Writing_Thread.Start(W);
                }

                // Threads that read the values.
                for (int R = 0; R < Nb_Reading_Threads; R++)
                {
                    var Reading_Thread = new Thread((state) =>
                    {
                        int t = (int)state;
                        char u = (char)((int)('A') + (t % 10));

                        try
                        {
                            for (int j = 0; j < Max; j++)
                            {
                                Data_Set DS = Safe_Data;

                                for (int i = 0; i < Data_Set.Size; i++)
                                {
                                    if (DS.T[i] != DS.T[0])
                                    {
                                        string Log = "";

                                        for (int k = 0; k < Data_Set.Size; k++)
                                            Log += DS.T[k] + " ";

                                        throw new Exception("Iteration " + (i + 1) + "\r\n" + Log);
                                    }
                                }

                                if (!Silent) Console.Write(u);
                            }
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("\r\n" + "Reading thread " + (t + 1) + " / " + ex.Message + "\r\n");
                        }

                        Completed[t] = true;
                    });

                    Reading_Thread.Start(R);
                }

                // Wait for all threads to complete.
                bool All_Completed = false;

                while (!All_Completed)
                {
                    All_Completed = true;

                    for (int i = 0; i < Nb_Threads; i++)
                        All_Completed &= Completed[i];
                }

                // END.
                Console.WriteLine("");
                Console.WriteLine("Done!");
                Console.ReadLine();

                // Toogle mode.
                switch (Lock_Mode)
                {
                    case Mode.Unsafe:

                        Lock_Mode = Mode.Mutex;
                        break;

                    case Mode.Mutex:

                        Lock_Mode = Mode.Slim;
                        break;

                    case Mode.Slim:

                        Lock_Mode = Mode.Unsafe;
                        break;
                }
            }
        }
    }
}

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM