Essential C# 5.0 Note - Chapter 18 & 19 - MultiThreading

How many ways for multi-thread synchronisation

For all other data types and non thread-safe resources, multithreading can only be safely performed using the constructs in this topic

Lock

The lock (C#) statements can be used to ensure that a block of code runs to completion without interruption by other threads.

Lock on an object

This is accomplished by obtaining a mutual-exclusion lock for a given object for the duration of the code block.

A lock statement is given an object as an argument, and is followed by a code block that is to be executed by only one thread at a time. For example:

public class TestThreading
{
private System.Object lockThis = new System.Object();

public void Process()
{


lock (lockThis)
{
// Access thread-sensitive resources.
}
}
}

The argument provided to the lock keyword must be an object based on a reference type, and is used to define the scope of the lock.

In the example above, the lock scope is limited to this function because no references to the object lockThis exist outside the function.

If such a reference did exist, lock scope would extend to that object.

Strictly speaking, the object provided is used solely to uniquely identify the resource being shared among multiple threads, so it can be an arbitrary class instance.

In practice, however, this object usually represents the resource for which thread synchronization is necessary.

E.G For example, if a container object is to be used by multiple threads, then the container can be passed to lock, and the synchronized code block following the lock would access the container.

As long as other threads locks on the same contain before accessing it, then access to the object is safely synchronized.

Generally, it is best to avoid
1.locking on a public type
2.locking on object instances beyond the control of your application.

E.G For example, lock(this) can be problematic if the instance can be accessed publicly, because code beyond your control may lock on the object as well.

This could create deadlock situations where two or more threads wait for the release of the same object.

Lock on public data type

Locking on a public data type, as opposed to an object, can cause problems for the same reason.

E.G Locking on literal strings is especially risky because literal strings are interned by the common language runtime (CLR).

This means that there is one instance of any given string literal for the entire program, the exact same object represents the literal in all running application domains, on all threads.

As a result, a lock placed on a string with the same contents anywhere in the application process locks all instances of that string in the application.

As a result, it is best to lock a private or protected member that is not interned.

Some classes provide members specifically for locking.

The Array type, for example, provides SyncRoot. Many collection types provide a SyncRoot member as well.

The lock keyword marks a statement block as a critical section by obtaining the mutual-exclusion lock for a given object, executing a statement, and then releasing the lock.

The following example includes a lock statement.

class Account
{
decimal balance;
private Object thisLock = new Object();

public void Withdraw(decimal amount)
{

lock (thisLock)
{
if (amount > balance)
{
throw new Exception("Insufficient funds");
}
balance -= amount;
}
}
}

The lock keyword ensures that one thread does not enter a critical section of code while another thread is in the critical section. If another thread tries to enter a locked code, it will wait, block, until the object is released.

The section Threading (C# and Visual Basic) discusses threading.

The lock keyword calls Enter at the start of the block and Exit at the end of the block. A ThreadInterruptedException is thrown if Interrupt interrupts a thread that is waiting to enter a lock statement.

In general, avoid locking on a public type, or instances beyond your code’s control. The common constructs lock (this), lock (typeof (MyType)), and lock (“myLock”) violate this guideline:
lock (this) is a problem if the instance can be accessed publicly. lock (typeof (MyType)) is a problem if MyType is publicly accessible.
*lock(“myLock”) is a problem because any other code in the process using the same string, will share the same lock.

Best practice is to define a private object to lock on, or a private static object variable to protect data common to all instances.

You can’t use the await keyword in the body of a lock statement.

//using System.Threading;

class ThreadTest
{
public void RunMe()
{

Console.WriteLine("RunMe called");
}

static void Main()
{

ThreadTest b = new ThreadTest();
Thread t = new Thread(b.RunMe);
t.Start();
}
}
// Output: RunMe called

// The following sample uses threads and lock. As long as the lock statement is present, the // statement block is a critical section and balance will never become a negative number.
class Account
{
private Object thisLock = new Object();
int balance;

Random r = new Random();

public Account(int initial)
{

balance = initial;
}

int Withdraw(int amount)
{


// This condition never is true unless the lock statement
// is commented out.
if (balance < 0)
{
throw new Exception("Negative Balance");
}

// Comment out the next line to see the effect of leaving out
// the lock keyword.
lock (thisLock)
{
if (balance >= amount)
{
Console.WriteLine("Balance before Withdrawal : " + balance);
Console.WriteLine("Amount to Withdraw : -" + amount);
balance = balance - amount;
Console.WriteLine("Balance after Withdrawal : " + balance);
return amount;
}
else
{
return 0; // transaction rejected
}
}
}

public void DoTransactions()
{

for (int i = 0; i < 100; i++)
{
Withdraw(r.Next(1, 100));
}
}
}

class Test
{
static void Main()
{

Thread[] threads = new Thread[10];
Account acc = new Account(1000);
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(new ThreadStart(acc.DoTransactions));
threads[i] = t;
}
for (int i = 0; i < 10; i++)
{
threads[i].Start();
}
}
}

Monitors

Like the lock and SyncLock keywords, monitors prevent blocks of code from simultaneous execution by multiple threads.

The Enter method allows one and only one thread to proceed into the following statements; all other threads are blocked until the executing thread calls Exit.

This is just like using the lock keyword.

lock (x)
{
DoSomething();
}

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}

Using the lock (C#) keyword is generally preferred over using the Monitor class directly, both because

  1. lock or SyncLock is more concise (adj. 简明的,简洁的; ; 简约; 精炼),
  2. And because lock or SyncLock insures that the underlying monitor is released, even if the protected code throws an exception. This is accomplished (by “exit”) with the finally keyword, which executes its associated code block regardless of whether an exception is thrown.

Synchronization Events and Wait Handles

Using a lock or monitor is useful for preventing the simultaneous execution of thread-sensitive blocks of code, but these constructs do not allow one thread to communicate an event to another.

This requires synchronization events, which are objects that have one of two states, signaled and un-signaled, that can be used to activate and suspend threads.

Threads can be suspended by being made to wait on a synchronization event that is unsignaled, and can be activated by changing the event state to signaled.

If a thread attempts to wait on an event that is already signaled, then the thread continues to execute without delay.

There are two kinds of synchronization events:
AutoResetEvent ManualResetEvent

They differ only in that AutoResetEvent changes from signaled to unsignaled automatically any time it activates a thread.

Conversely, a ManualResetEvent allows any number of threads to be activated by its signaled state, and will only revert to an unsignaled state when its Reset method is called.

Threads can be made to wait on events by calling one of the wait methods, such as WaitOne, WaitAny, or WaitAll.

WaitHandle.WaitOne causes the thread to wait until a single event becomes signaled, WaitHandle.WaitAny blocks a thread until one or more indicated events become signaled, *WaitHandle.WaitAll blocks the thread until all of the indicated events become signaled.

An event becomes signaled when its Set method is called.

In the following example, a thread is created and started by the Main function.

The new thread waits on an event using the WaitOne method.

The thread is suspended until the event becomes signaled by the primary thread that is executing the Main function.

Once the event becomes signaled, the auxiliary thread returns.

In this case, because the event is only used for one thread activation, either the AutoResetEvent or ManualResetEvent classes could be used.

using System;
using System.Threading;

class ThreadingExample
{
static AutoResetEvent autoEvent;

static void DoWork()
{

Console.WriteLine(" worker thread started, now waiting on event...");
autoEvent.WaitOne();
Console.WriteLine(" worker thread reactivated, now exiting...");
}

static void Main()
{

autoEvent = new AutoResetEvent(false);

Console.WriteLine("main thread starting worker thread...");
Thread t = new Thread(DoWork);
t.Start();

Console.WriteLine("main thread sleeping for 1 second...");
Thread.Sleep(1000);

Console.WriteLine("main thread signaling worker thread...");
autoEvent.Set();
}
}

Mutex Object

A mutex is similar to a monitor; it prevents the simultaneous execution of a block of code by more than one thread at a time. In fact, the name “mutex” is a shortened form of the term “mutually exclusive.”

Unlike monitors, however, a mutex can be used to synchronize threads across processes.

A mutex is represented by the Mutex class.

When used for inter-process synchronization, a mutex is called a named mutex because it is to be used in another application, and therefore it cannot be shared by means of a global or static variable.

It must be given a name so that both applications can access the same mutex object.

Although a mutex can be used for intra-process thread synchronization, using Monitor is generally preferred, because monitors were designed specifically for the .NET Framework and therefore make better use of resources.

In contrast, the Mutex class is a wrapper to a Win32 construct.

While it is more powerful than a monitor, a mutex requires interop transitions that are more computationally expensive than those required by the Monitor class.

For an example of using a mutex, see Mutexes.

Interlocked

For simple operations on integral numeric data types, synchronizing threads can be accomplished with members of the Interlocked class.

You can use the methods of the Interlocked class to prevent problems that can occur when multiple threads attempt to simultaneously update or compare the same value.

The methods of this class let you safely increment, decrement, exchange, and compare values from any thread.

The methods of this class help protect against errors that can occur when the scheduler switches contexts while a thread is updating a variable that can be accessed by other threads, or when two threads are executing concurrently on separate processors.

The members of this class do not throw exceptions.

The Increment and Decrement methods increment or decrement a variable and store the resulting value in a single operation.

On most computers, incrementing a variable is not an atomic operation, requiring the following steps:
1.Load a value from an instance variable into a register.
2.Increment or decrement the value.
3.Store the value in the instance variable.

If you do not use Increment and Decrement, a thread can be preempted after executing the first two steps.

Another thread can then execute all three steps. When the first thread resumes execution, it overwrites the value in the instance variable, and the effect of the increment or decrement performed by the second thread is lost.

The Exchange method atomically exchanges the values of the specified variables.

The CompareExchange method combines two operations: comparing two values and storing a third value in one of the variables, based on the outcome of the comparison.

The compare and exchange operations are performed as an atomic operation.

using System;
using System.Threading;

namespace InterlockedExchange_Example
{
class MyInterlockedExchangeExampleClass
{
//0 for false, 1 for true.
private static int usingResource = 0;

private const int numThreadIterations = 5;
private const int numThreads = 10;

static void Main()
{

Thread myThread;
Random rnd = new Random();

for(int i = 0; i < numThreads; i++)
{
myThread = new Thread(new ThreadStart(MyThreadProc));
myThread.Name = String.Format("Thread{0}", i + 1);

//Wait a random amount of time before starting next thread.
Thread.Sleep(rnd.Next(0, 1000));
myThread.Start();
}
}

private static void MyThreadProc()
{

for(int i = 0; i < numThreadIterations; i++)
{
UseResource();

//Wait 1 second before next attempt.
Thread.Sleep(1000);
}
}

//A simple method that denies reentrancy.
static bool UseResource()
{

//0 indicates that the method is not in use.
if(0 == Interlocked.Exchange(ref usingResource, 1))
{
Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name);

//Code to access a resource that is not thread safe would go here.

//Simulate some work
Thread.Sleep(500);

Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name);

//Release the lock
Interlocked.Exchange(ref usingResource, 0);
return true;
}
else
{
Console.WriteLine(" {0} was denied the lock", Thread.CurrentThread.Name);
return false;
}
}

}
}

ReaderWriter Locks

In some cases, you may want to lock a resource only when data is being written and permit multiple clients to simultaneously read data when data is not being updated.

The ReaderWriterLock class enforces exclusive access to a resource while a thread is modifying the resource, but it allows non-exclusive access when reading the resource.

ReaderWriter locks are a useful alternative to exclusive locks, which cause other threads to wait, even when those threads do not need to update data.

Deadlocks

Thread synchronization is invaluable in multithreaded applications, but there is always the danger of creating a deadlock, where multiple threads are waiting for each other and the application comes to a halt.

A deadlock is analogous to a situation in which cars are stopped at a four-way stop and each person is waiting for the other to go.

Avoiding deadlocks is important; the key is careful planning.

You can often predict deadlock situations by diagramming multithreaded applications before you start coding.