Labels

Monday, June 15, 2009

Threading || Wait Handle

Wait Handles

 

Before we proceed further, better to understand the ‘WaitHandle’ Object.

 

 

 

 

The Win32 API has a richer set of synchronization constructs, and these are exposed in the .NET framework via the EventWaitHandle, Mutex and Semaphore classes.

Some are more useful than others: the Mutex class, for instance, mostly doubles up on what's provided by lock, while EventWaitHandle provides unique signaling functionality.

 

All three classes are based on the abstract WaitHandle class, although behaviorally, they are quite different.

One of the things they do all have in common - Allows to work across all processes, rather than across just the threads in the current process.

 

Note – Mutex & Semaphore – Covered under ‘Threading || Basic Synchronization’ post. Thus here we’ll cover only ‘EventWaitHandle’.

 

 

A). EventWaitHandle - AutoResetEvent

 

An AutoResetEvent is much like a ticket turnstile: Inserting a ticket lets exactly only one person through.

The "auto" in the class's name refers to the fact that an open turnstile automatically closes or "resets" after someone is let through using ‘Set’  – i.e At a time it allows only one waiting thread to get in.

 

A thread waits, or blocks, at the turnstile by calling WaitOne and a ticket is inserted by calling the Set method.

If a number of threads call WaitOne, a queue builds up behind the turnstile.

A ticket can come from any thread – in other words, any (unblocked) thread with access to the AutoResetEvent object can call Set on it to release one blocked thread.

 

If Set is called when no thread is waiting, the handle stays open for as long as it takes until some thread to call WaitOne.

 

An AutoResetEvent can be created in one of two ways.

 

EventWaitHandle wh = new AutoResetEvent (false);

EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto);

 

Simple Example –

 

 

 

class BasicWaitHandle

    {

        static EventWaitHandle wh = new AutoResetEvent(false);

        static void Main()

        {

            new Thread(Waiter).Start();

            Thread.Sleep(5000); // Wait for some time...

            wh.Set(); // OK - wake it up

        }

        static void Waiter()

        {

            Console.WriteLine("Waiting...");

            wh.WaitOne(); // Wait for notification

            Console.WriteLine("Notified");

        }

    }

 

Output –

 

Waiting... (pause) Notified.

 

 

 

Producer/Consumer Queue

 

Another common threading scenario is to have a background worker process tasks from a queue. This is called a Producer/Consumer queue: the producer enqueues tasks; the consumer dequeues tasks on a worker thread. It's rather like the previous example, except that the caller doesn't get blocked if the worker's already busy with a task.

 

A Producer/Consumer queue is scaleable, in that multiple consumers can be created – each servicing the same queue, but on a separate thread.

This is a good way to take advantage of multiprocessor systems while still restricting the number of workers so as to avoid the pitfalls of unbounded concurrent threads (excessive context switching and resource contention).

 

In the example below, a single AutoResetEvent is used to signal the worker, which waits only if it runs out of tasks (when the queue is empty).

A generic collection class is used for the queue, whose access must be protected by a lock to ensure thread-safety.

 

Note - The worker is ended by enqueing a null task. (Here below is end of ‘Using’ block in class ‘Test’

 

 

 

class ProducerConsumerQueue : IDisposable

    {

        EventWaitHandle wh = new AutoResetEvent(false);

        Thread consumer;

        object locker = new object();

        Queue<string> tasks = new Queue<string>();

 

        public ProducerConsumerQueue()

        {

            consumer = new Thread(Consumer);

            consumer.Start(); // Constructor Starting Thread

        }

 

        public void Producer(string task)

        {

            lock (locker)

            {

              tasks.Enqueue(task);

            }

            wh.Set();

        }

        public void Dispose()

        {

            EnqueueTask(null); // Signal the consumer to exit.

            worker.Join(); // Wait for the consumer's thread to finish.

            wh.Close(); // Release any OS resources.

        }

 

        void Consumer()

        {

            while (true)

            {

                string task = null;

                lock (locker)

                {

                    if (tasks.Count > 0)

                    {

                        task = tasks.Dequeue();

                        if (task == null)

                        return;

                    }

                }

                if (task != null)

                {

                    Console.WriteLine("Performing task: " + task);

                    Thread.Sleep(1000); // simulate work...

                }

                else

                {

                    wh.WaitOne(); // No more tasks - wait for a signal

                }

            } // End While

        }

    }

 

 

 

class Test

    {

        static void Main()

        {

            using (ProducerConsumerQueue q = new ProducerConsumerQueue())

            {

                q. Producer ("Hello");

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

                {

                        q. Producer("Say " + i);

                }

                q. Producer("Goodbye!");

            }

 

            // Exiting the using statement calls q's Dispose method, which

            // enqueues a null task and waits until the consumer finishes.

        }

    }

 

Output –

 

Performing task: Hello

Performing task: Say 1

Performing task: Say 2

Performing task: Say 3

...

...

Performing task: Say 9

Goodbye!

 

 

 

 

 

B). EventWaitHandle - ManualResetEvent

 

A ManualResetEvent is a variation on AutoResetEvent. It differs in that it doesn't automatically reset after one thread is let through using ‘Set’ on a WaitOne call, and so functions like a gate: calling Set opens the gate, Allowing Any Number Of Threads that WaitOne at the gate through; calling Reset closes the gate, causing, potentially, a queue of waiters to accumulate until its next opened.

 

 

Hope this helps.

 

Thanks & Regards,

Arun Manglick || Senior Tech Lead

 

 

No comments:

Post a Comment