Labels

Monday, June 15, 2009

Threading || Basic Synchronization

Scope of Work

 

·         Various Synchronization Techniques

·         Synchronization Essentials                     

1.      Blocking                                               

2.      Sleeping and Spinning                           

3.      Joining a Thread

                 

·         Locking and Thread Safety                    

1.      Choosing the Synchronization Object     

2.      Nested Locking                                     

3.      When to Lock                                       

4.      Performance Considerations                  

5.      Thread Safety                                       

 

·         Interrupt and Abort                               

1.      Interrupt                                              

2.      Abort                                                   

 

·         Thread State                                        

 

·         Wait Handles                                        

1.      AutoResetEvent                                    

2.      ManualResetEvent                                

3.      Mutex                                                  

4.      Semaphore                                           

5.      WaitAny, WaitAll and SignalAndWait      

 

·         Synchronization Contexts                      

1.      Reentrancy                                           

 

 

 

Various Synchronization Techniques

 

Simple Blocking

Sleep

 

Blocks for a given time period.

 

Join

 

Waits for another thread to finish.

 

 

Locking

 

Purpose

Cross- Process?

Speed

lock

 

Ensures just one thread can access a resource, or section of code.

 

No

Fast

Mutex

 

Ensures just one thread can access a resource, or section of code.

Can be used to prevent multiple instances of an application from

starting.

 

 

Yes

Moderate

Semaphore

 

Ensures not more than a specified number of threads can access a

resource, or section of code.

 

Yes

Moderate

 

 

Signaling

 

Purpose

Cross- Process?

Speed

EventWaitHandle

 

Allows a thread to wait until it receives a signal from another thread.

 

Yes

Moderate

Wait and Pulse

 

Allows a thread to wait until a custom blocking condition is met.

 

No

Moderate

 

 

Non-Blocking Synchronization

 

Purpose

Cross- Process?

Speed

Interlocked

 

To perform simple non-blocking atomic operations.

 

Yes

Very Fast

volatile

 

To allow safe non-blocking access to individual fields outside of a lock.

 

Yes

Very Fast

 

 

A). Simple Blocking – Sleep & Join

 

 

  • When a thread waits or pauses as a result of using either the Sleep/Join constructs, it's said to be Blocked.
  • Once blocked, a thread immediately relinquishes its allocation of CPU time, adds WaitSleepJoin to its ThreadState property, and doesn’t get re-scheduled until unblocked.

 

  • Unblocking happens in one of four ways (the computer's power button doesn't count!):
    • by the blocking condition being satisfied
    • by the operation timing out (if a timeout is specified)
    • by being interrupted via Thread.Interrupt
    • by being aborted via Thread.Abort

 

A thread is not considered to be blocked if its execution is paused via the (deprecated) Suspend method.

 

Sleeping a Thread

 

  • Thread.Sleep blocks the current thread for the given time period (or until interrupted).
  • Thread.Sleep relinquishes the CPU, requesting that the thread is not rescheduled until the given time period has elapsed.
  • Thread.Sleep(0) relinquishes the CPU just long enough to allow any other active threads present in a time-slicing queue (should there be one) to be executed.

 

 

 

class ThreadSafe

    {       

        static void Main()

        {

            Thread.Sleep(0); // relinquish CPU time-slice

            Thread.Sleep(1000); // sleep for 1000 milliseconds

            Thread.Sleep(TimeSpan.FromHours(1)); // sleep for 1 hour

            Thread.Sleep(Timeout.Infinite); // sleep until interrupted

        }

 

    }

 

 

 

 

 

Joining a Thread

 

  • You can block until another thread ends by calling Join:
  • The Join method also accepts a timeout argument – in milliseconds, or as a TimeSpan, returning false if the Join timed out rather than found the end of the thread.

 

 

class JoinDemo

    {

        static void Main()

        {

            Thread t = new Thread(delegate() { Console.ReadLine(); });

            t.Start();

            t.Join(); // Main thread should wait to join thread t, until thread t finishes

            Console.WriteLine("Thread t's ReadLine complete!");

        }

    }

 

Here – Bcoz of Join, the write statement will not be displayed unless you hit enter.

 

 

 

 

 

Spinning a Thread

 

  • The Thread class also provides a SpinWait method, which doesn’t relinquish any CPU time, instead looping the CPU – keeping it “uselessly busy” for the given number of iterations.
  • 50 iterations might equate to a pause of around a microsecond, although this depends on CPU speed and load.
  • Technically, SpinWait is not a blocking method: a spin-waiting thread does not have a ThreadState of WaitSleepJoin and can’t be prematurely Interrupted by another thread.
  • SpinWait is rarely used – its primary purpose being to wait on a resource that’s expected to be ready very soon (inside maybe a microsecond) without calling Sleep and wasting CPU time by forcing a thread change.

 

  • However this technique is advantageous only on multi-processor computers: on single-processor computers, there’s no opportunity for a resource’s status to change until the spinning thread ends its time-slice – which defeats the purpose of spinning to begin with. And calling SpinWait often or for long periods of time itself is wasteful on CPU time.

 

B). Locking and Thread Safety

 

Locking

 

  • Locking enforces exclusive access, and is used to ensure only one thread can enter particular sections of code at a time.
  • Only one thread can lock the synchronizing object (in this case locker) at a time.
  • A lock can only be released from the same thread that obtained it.
  • A thread blocked while awaiting a contended lock has a ThreadState of WaitSleepJoins.

 

However a thread blocked by lock can be forcibly released via another thread calling its Thread.Interrupt or Thread.Abort method.

 

 

 

class ThreadSafe

    {

        static object locker = new object();

        static int val1, val2;

        static void Go()

        {

            lock (locker)

            {

                if (val2 != 0)

                {

                    Console.WriteLine(val1 / val2);

                }

                val2 = 0;

            }

        }

    }

 

 

 

 

Locking doesn't restrict access to the synchronizing object itself in any way.

 

  • Choosing the Synchronization Object

 

    • Must be a reference type
    • Synchronizing object must be privately scoped to the class
    • Using the object or type itself as a synchronization object is discouraged because it potentially offers public scope to the synchronization object. The below is discouraged.

 

 

 

lock (this) { ... } 

or

lock (typeof (Widget)) { ... } // For protecting access to statics

 

 

 

 

 

 

Nested Locking –

 

  • The object can be released either when a corresponding number of unlocks statements have executed, or the outermost lock statement has exited

 

 

class ThreadSafe

    {

        static object x = new object();

        static void Main()

        {

            lock (x)

            {

                Console.WriteLine("I have the lock");

                Nest();

                Console.WriteLine("I still have the lock");

            }

 

            // Here the lock is released.

        }

        static void Nest()

        {

            lock (x)

            {

            }

        }

    }

 

 

 

Performance Consideration –

 

  • Locking itself is very fast, but due to locking the thread switching is time consuming.
  • Adverse effects
    • Impoverished concurrency - Occurs when too much code is placed in a lock statement, causing other threads to block unnecessarily.
    • Deadlocks - Occurs when two threads each wait for a lock held by the other, and so neither can proceed.
    • Lock race - Occurs when it’s possible for either of two threads to obtain a lock first, the program breaking if the “wrong” thread wins.

 

 

Mutex

 

  • Mutex provides the same functionality as C#'s lock statement.
  • As same as lock, a Mutex can only be released from the same thread that obtained it.

 

  • Advantages
    • Work across multiple processes to ensure that only one instance of program can run at a time.
    • A good feature of Mutex is that if the application terminates without ReleaseMutex first being called, the CLR will release the Mutex automatically.

 

  • Disadavantages.
    • While Mutex is reasonably fast, lock is a hundred times faster again.

 

 

 

class OneAtATimePlease

    {

        static Mutex mutex = new Mutex(false, "oreilly.com OneAtATimeDemo");

        static void Main()

        {

            // Wait 5 seconds if contended – in case another instance of the

           //  program is in the process of shutting down.

            if (!mutex.WaitOne(TimeSpan.FromSeconds(5), false))

            {

                Console.WriteLine("Another instance of the app is running. Bye!");

                return;

            }

            try

            {

                Console.WriteLine("Running - press Enter to exit");

                Console.ReadLine();

            }

            finally { mutex.ReleaseMutex(); }

        }

    }

 

Output –

 

Running - press Enter to exit

 

 

 

 

 

 

Semaphore

 

  • A Semaphore is like a nightclub: it has a certain capacity, enforced by a bouncer. Once full, no more people can enter the nightclub and a queue builds up outside. Then, for each person that leaves, one person can enter from the head of the queue.
  • The constructor requires a minimum of two arguments – the number of places currently available in the nightclub, and the nightclub's total capacity.
  • A Semaphore with a capacity of one is similar to a Mutex or lock, except that the Semaphore has no "owner" – it's thread-agnostic.

 

Any thread can call Release on a Semaphore, while with Mutex and lock, only the thread that obtained the resource can release it.

 

 

           

 

class SemaphoreTest

    {

        static Semaphore s = new Semaphore(3, 3); // Available=3; Capacity=3

        static void Main()

        {

            for (int i = 0; i < 10; i++)

            {

                new Thread(Go).Start();

            }

        }

        static void Go()

        {

            while (true)

            {

                s.WaitOne();

                Thread.Sleep(100); // Only 3 threads can get here at once            

                s.Release();

            }

        }

    }

 

 

 

 

D). Non-Blocking Synchronization

 

  • Locking can be used for achieving synchronization for even the simple cases like assigning or incrementing a field.
  • Although locking can always satisfy this need, a contended lock means that a thread must block, suffering the overhead and latency of being temporarily descheduled.
  • The non-blocking synchronization constructs can perform simple operations without ever blocking, pausing, or waiting.

 

Note - These involve using instructions that are strictly atomic, and instructing the compiler to use "volatile" read and write semantics.

 

 

Interlocked

 

  • A statement is atomic if it executes as a single indivisible instruction. Strict atomicity precludes any possibility of preemption.
  • In C#, a simple read or assignment on a field of 32 bits or less is atomic (assuming a 32-bit CPU).
  • Operations on larger fields are non-atomic, as are statements that combine more than one read/write operations. For e.g. Reading and writing 64-bit fields is non-atomic on 32-bit CPUs in the sense that two separate 32-bit memory locations are involved. If thread A reads a 64-bit value while thread B is updating it, thread A may end up with a bitwise combination of the old and new values.
  • Operatiosn that require Read & Write operations are also non-atomic.

 

 

 

class Atomicity

    {

        static int x, y;

        static long z;

        static void Test()

        {

            long myLocal;

            x = 3;                // Atomic

            z = 3;                // Non-atomic (z is 64 bits)

            myLocal = z;      // Non-atomic (z is 64 bits)

            y += x;              // Non-atomic (read AND write operation)

            x++;                  // Non-atomic (read AND write operation)

        }

    }

 

 

 

  • One way to solve to these problems is to wrap the non-atomic operations around a lock statement.
  • Locking, in fact, simulates atomicity. The Interlocked class, however, provides a simpler and faster solution for simple atomic operations.

 

 

 

class Program

    {

        static long sum;

        static void Main()

        {

            // Simple increment/decrement operations:

            Interlocked.Increment(ref sum); // 1

            Interlocked.Decrement(ref sum); // 0

 

            // Add/subtract a value:

            Interlocked.Add(ref sum, 3); // 3

 

            // Read a 64-bit field:

            Console.WriteLine(Interlocked.Read(ref sum)); // 3

 

            // Write a 64-bit field while reading previous value:

            // (This prints "3" while updating sum to 10)

            Console.WriteLine(Interlocked.Exchange(ref sum, 10)); // 10

 

            // Update a field only if it matches a certain value (10):

            Interlocked.CompareExchange(ref sum, 123, 10); // 123

        }

    }

 

 

 

 

  • Using Interlocked is generally more efficient that obtaining a lock, because it can never block and suffer the overhead of its thread being temporarily descheduled.
  • Interlocked is also valid across multiple processes – in contrast to the lock statement, which is effective only across threads in the current process. An example of where this might be useful is in reading and writing into shared memory.

 

 

Hope this is clear now.

 

Thanks & Regards,

Arun Manglick || Senior Tech Lead

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

No comments:

Post a Comment