Pergunta

I have a property with a backing field which I want to make thread safe (get and set). The get and set method has no logic except the setting and returning.

I think there are two ways to capsule the logic in the property self (volatile and lock). Is my understanding of the two's correct or have I make any mistakes?

Below are my examples:

    public class ThreadSafeClass
    {
        // 1. Volatile Example:

        private volatile int _processState_1;
        public int ProcessState_1
        {
            get { return _processState_1; }
            set { _processState_1 = value; }
        }

        // 2. Locking Example:

        private readonly object _processState_2Lock = new object();
        private int _processState_2;
        public int ProcessState_2
        {
            get
            {
                lock (_processState_2Lock)
                {
                    return _processState_2;
                }
            }
            set
            {
                lock (_processState_2Lock)
                {
                    _processState_2 = value;
                }
            }
        }
    }
Foi útil?

Solução

For mor information see the great site by J. Albahari:

Synchronization constructs can be divided into four categories:

Simple blocking methods:

These wait for another thread to finish or for a period of time to elapse. Sleep, Join, and Task.Wait are simple blocking methods.

Locking constructs:

These limit the number of threads that can perform some activity or execute a section of code at a time. Exclusive locking constructs are most common — these allow just one thread in at a time, and allow competing threads to access common data without interfering with each other. The standard exclusive locking constructs are lock (Monitor.Enter/Monitor.Exit), Mutex, and SpinLock. The nonexclusive locking constructs are Semaphore, SemaphoreSlim, and the reader/writer locks.

Signaling constructs:

These allow a thread to pause until receiving a notification from another, avoiding the need for inefficient polling. There are two commonly used signaling devices: event wait handles and Monitor’s Wait/Pulse methods. Framework 4.0 introduces the CountdownEvent and Barrier classes.

Non-blocking synchronization constructs:

These protect access to a common field by calling upon processor primitives. The CLR and C# provide the following nonblocking constructs: Thread.MemoryBarrier, Thread.VolatileRead, Thread.VolatileWrite, the volatile keyword, and the Interlocked class.


The volatile keyword:

The volatile keyword instructs the compiler to generate an acquire-fence on every read from that field, and a release-fence on every write to that field. An acquire-fence prevents other reads/writes from being moved before the fence; a release-fence prevents other reads/writes from being moved after the fence. These “half-fences” are faster than full fences because they give the run-time and hardware more scope for optimization.

As it happens, Intel’s X86 and X64 processors always apply acquire-fences to reads and release-fences to writes — whether or not you use the volatile keyword — so this keyword has no effect on the hardware if you’re using these processors. However, volatile does have an effect on optimizations performed by the compiler and the CLR — as well as on 64-bit AMD and (to a greater extent) Itanium processors. This means that you cannot be more relaxed by virtue of your clients running a particular type of CPU.

The effect of applying volatile to fields can be summarized as follows:

First instruction   Second instruction  Can they be swapped?
Read                Read                No
Read                Write               No
Write               Write               No (The CLR ensures that write-write operations are never swapped, even without the volatile keyword)
Write               Read                Yes!

Notice that applying volatile doesn’t prevent a write followed by a read from being swapped, and this can create brainteasers. Joe Duffy illustrates the problem well with the following example: if Test1 and Test2 run simultaneously on different threads, it’s possible for a and b to both end up with a value of 0 (despite the use of volatile on both x and y):

class IfYouThinkYouUnderstandVolatile
{
  volatile int x, y;

  void Test1()        // Executed on one thread
  {
    x = 1;            // Volatile write (release-fence)
    int a = y;        // Volatile read (acquire-fence)
    ...
  }

  void Test2()        // Executed on another thread
  {
    y = 1;            // Volatile write (release-fence)
    int b = x;        // Volatile read (acquire-fence)
    ...
  }
}

The MSDN documentation states that use of the volatile keyword ensures that the most up-to-date value is present in the field at all times. This is incorrect, since as we’ve seen, a write followed by a read can be reordered.

This presents a strong case for avoiding volatile: even if you understand the subtlety in this example, will other developers working on your code also understand it? A full fence between each of the two assignments in Test1 and Test2 (or a lock) solves the problem.

The volatile keyword is not supported with pass-by-reference arguments or captured local variables: in these cases you must use the VolatileRead and VolatileWrite methods.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top