简体   繁体   中英

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

What is the safest (and shortest) way do lock read/write access to static members in a multithreaded environment in C#?

Is it possible to do the threadsafe locking & unlocking on class level (so I don't keep repeating lock/unlock code every time static member access is needed)?

Edit : Sample code would be great :)

Edit : Should I use the volatile keyword or Thread.MemoryBarrier() to avoid multiprocessor caching or is that unnecessary? According to Jon Skeet only those will make changes visible to other processors? (Asked this separately here ).

Small Values

For small values (basically any field that can be declared volatile), you can do the following:

private static volatile int backingField;

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

Large Values

With large values the assignment won't be atomic if the value is larger then 32-bits on a 32-bit machine or 64-bits on a 64-bit machine. See the ECMA 335 12.6.6 spec. So for reference types and most of the built-in value types the assignment is atomic, however if you have some large struct, like:

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

In this case you will need some kind of locking around the get accessor. You could use ReaderWriterLockSlim for this which I've demonstrated below. Joe Duffy has advice on using ReaderWriterLockSlim vs ReaderWriterLock :

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

Unsafe Get-Accessor Demonstration

Here's the code I used to show the lack of atomicity when not using a lock in the 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();
    }

The safest and shortest way is to create a private, static field of type Object that is only used for locking (think of it as a "pad-lock" object). Use this and only this field to lock on as this prevent other types from locking up your code when then lock on the same type that you do.

If you lock on the type itself there is risk that another type will also decide to lock on your type and this could create deadlocks.

Here is an example:

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

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

Notice that I have create a private, static field for locking foo - I use that field to lock the write operations on that field.

Although you could just use a single mutex to control all the access to the class (effectively serializing the access to the class) I suggest you study the static class, determine which members are being used where and how and the use one or several ReaderWriterLock (code examples in the MSDN documentation) which provides access to several readers but only one writer at the same time.

That way you'll have a fine grained multithreaded class which will only block for writing but will allow several readers at the same time and which will allow writing to one member while reading another unrelated member.

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 on an object specifically created for this purpose rather than on typeof(LockExample) to prevent deadlock situations where others have locked on LockExample 's type object.

Is it possible to do the threadsafe locking & unlocking on class level (so I don't keep repeating lock/unlock code every time static member access is needed)?

Only lock where you need it, and do it inside the callee rather than requiring the caller to do the lock ing.

Several others have already explained how to use the lock keyword with a private lock object, so I will just add this:

Be aware that even if you lock inside each method in your type, calling more than one method in a sequence can not be considered atomic. For example if you're implementing a dictionary and your interface has a Contains method and an Add method, calling Contains followed by Add will not be atomic. Someone could modify the dictionary between the calls to Contains and Add - ie there's a race condition. To work around this you would have to change the interface and offer a method like AddIfNotPresent (or similar) which encapsulates both the checking and the modification as a single action.

Jared Par has an excellent blog post on the topic (be sure to read the comments as well).

You should lock/unlock on each static member access, within the static accessor, as needed.

Keep a private object to use for locking, and lock as required. This keeps the locking as fine-grained as possible, which is very important. It also keeps the locking internal to the static class members. If you locked at the class level, your callers would become responsible for the locking, which would hurt usability.

I thank you all and I'm glad to share this demo program, inspired by the above contributions, that run 3 modes (not safe, mutex, slim).

Note that setting "Silent = false" will result in no conflict at all between the threads. Use this "Silent = false" option to make all threads write in the Console.

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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