Scope of Work
· How Threading Works
· Threads vs. Processes
· Approaches of Creating Thread
· Approaches - Passing Data to ThreadStart
·
· Foreground and Background Threads
· Thread Priority
· Approaches – Exception Handling
· Approaches of How Threads share data
· Approaches of Thread Safety when Threads share data
Note -
§ A C# prodgram starts in a single thread created automatically by the CLR and operating system (the "main" thread), and is made multithreaded by creating additional threads.
§ A C# application can become multi-threaded in two ways:
o Explicitly - By creating and running additional threads, or
o Implicitly - By using a feature of the .NET framework that implicitly creates threads – such as BackgroundWorker, thread pooling, a threading timer, a Remoting server, or a Web Services or ASP.NET application. In these latter cases, one has no choice but to embrace multithreading.
How Threading Works
§ Threading is managed internally by a Thread Scheduler, a function the CLR typically delegates to the operating system. A thread scheduler ensures all active threads are allocated appropriate execution time.
§ On a single-processor computer, a thread scheduler performs Time-Slicing – rapidly switching execution between each of the active threads. Under Windows XP, a time-slice is typically in the tens-of milliseconds region.
§ On a multi-processor computer, multithreading is implemented with a Mixture Of Time-Slicing And Genuine Concurrency – where different threads run code simultaneously on different CPUs.
Threads vs. Processes
§ All threads within a single application are logically contained within a Process – the operating system unit in which an application runs.
§ Threads have certain similarities to processes – for instance, processes are typically time-sliced with other processes running on the computer in much the same way as threads within a single C# application.
§ The key difference is that processes are fully isolated from each other; Threads Share (Heap) Memory with other threads running in the same application. This is what makes threads useful: one thread can be fetching data in the background, while another thread is displaying the data as it arrives.
When to Use Threads
- A common application for multithreading is performing time-consuming tasks in the background.
- The main thread keeps running, while the worker thread does its background job.
- With Windows Forms applications, if the main thread is tied up performing a lengthy operation, keyboard and mouse messages cannot be processed, and the application becomes unresponsive.
- In the case of non-UI applications, such as a Windows Service, multithreading makes particular sense when a task is potentially time-consuming because it’s awaiting a response from another computer (such as an application server, database server, or client). Having a worker thread perform the task means the instigating thread is immediately free to do other things.
- Another use for multithreading is in methods that perform intensive calculations. Such methods can execute faster on a multi-processor computer if the workload is divided amongst multiple threads.
A C# application can become multi-threaded in two ways:
Explicitly – By creating and running additional threads, or
Implicitly – By using a feature of the .NET framework that implicitly creates threads – such as BackgroundWorker, thread pooling, a threading timer, a Remoting server, or a Web Services or ASP.NET application.
In these latter cases, one has no choice but to embrace multithreading.
When Not to Use Threads
- Multithreading also comes with disadvantages.
- Vastly more complex programs.
- Having multiple threads does not in itself create complexity; it's the interaction between the threads that creates complexity.
- Multithreading also comes with a resource and CPU cost in allocating and switching threads if used excessively. In particular, when heavy disk I/O is involved, it can be faster to have just one or two workers thread performing tasks in sequence, rather than having a multitude of threads each executing a task at the same time.
Approaches of Creating Thread:
A C# program starts in a single thread created automatically by the CLR and operating system (the "main" thread), and is made multithreaded by creating additional threads.
Below are the few ways of swapning a new thread.
Using ThreadStart Delegate:
| class ThreadTest { static void { Thread t = new Thread (new ThreadStart (Go)); t.Start(); // Run Go() on the new thread. Go(); // Simultaneously run Go() in the main thread. } static void Go() { Console.WriteLine ("hello!"); } } Output – hello! hello! |
| |
Shortcut syntax: In this case, a ThreadStart delegate is inferred automatically by the compiler.
| class ThreadTest { static void { Thread t = new Thread (Go); t.Start(); // Run Go() on the new thread. Go(); // Simultaneously run Go() in the main thread. } static void Go() { Console.WriteLine ("hello!"); } } Output – hello! hello! |
| |
Anonymous Delegate method to start the thread:
| class ThreadTest { static void { Thread t = new Thread (delegate() { Console.WriteLine ("Auto Hello!"); }); t.Start(); // Run Go() on the new thread. Go(); // Simultaneously run Go() in the main thread. } static void Go() { Console.WriteLine ("hello!"); } } Output – Auto hello! hello! |
| |
Approaches - Passing Data to ThreadStart
Using – ParameterizedThreadStart delegate:
We couldn’t use the ThreadStart delegate because it doesn’t accept arguments.
Fortunately, the .NET framework defines another version of the delegate called ParameterizedThreadStart, which accepts a single object argument as follows:
| public delegate void ParameterizedThreadStart (object obj); |
| class ThreadTest { static void { Thread t = new Thread (Go); t.Start(true); // == Go (true) Go(false); // Simultaneously run Go() in the main thread. } static void Go (object upperCase) { bool upper = (bool) upperCase; Console.WriteLine (upper ? "HELLO!" : "hello!"); } } Output – hello! Hello! |
| In the above example, the compiler automatically infers a ParameterizedThreadStart delegate because the Go method accepts a single object argument. Or otherwise we could just as well have written: Thread t = new Thread (new ParameterizedThreadStart (Go)); t.Start(true); |
· A feature of using ParameterizedThreadStart is that we must cast the object argument to the desired type (in this case bool) before use.
· Also, there is only a single-argument version of this delegate.
Using – Anonymouse Methods
An alternative is to use an anonymous method to call an ordinary method as follows:
| class ThreadTest { static void { String name; Int age; Thread t = new Thread (delegate() { WriteText (name, age); }); name = “Arun”; age = 30; t.Start(); Thread.Sleep(5000); name = “Ruchi”; age = 27; WriteText(name, age); // Simultaneously run Go() in the main thread. } static void WriteText (string name, int age) { Console.WriteLine (name + “:” + age.ToString()); } } Output – Arun : 30 Ruchi : 27 |
| |
The advantage is that –
· the target method (in this case WriteText) can accept any number of arguments, and
· no casting is required.
Using – Instance Methods
Another common system for passing data to a thread is by giving Thread an instance method rather than a static method. The instance object’s properties can then tell the thread what to do, as in the following rewrite of the original example:
| class ThreadTest { String name; int age; static void { ThreadTest instance1 = new ThreadTest(); instance1.name = "Arun"; instance1.age = 30; Thread t = new Thread(instance1.Go); t.Start(); Thread.Sleep(5000); ThreadTest instance2 = new ThreadTest(); instance2.name = "Ruchi"; instance2.age = 29; instance2.Go(); // Main thread – runs with upper=false } void Go() { Console.WriteLine(name + ":" + age.ToString()); } } Output – Arun : 30 Ruchi : 29 |
| |
Naming Threads
· A thread can be named via its Name property.
· A thread’s name can be set at any time – but only once – attempts to subsequently change it will throw an exception.
| class ThreadNaming { static void { Thread.CurrentThread.Name = "main"; Thread worker = new Thread(Go); worker.Name = "worker"; worker.Start(); Thread.Sleep(1000); Go(); } static void Go() { Console.WriteLine("Hello from " + Thread.CurrentThread.Name); } } Output – Hello from worker Hello from main |
| |
Foreground and Background Threads
· By default, threads are foreground threads, meaning they keep the application alive for as long as any one of them is running.
· On the other hand background threads, don’t keep the application alive on their own – terminating immediately once all foreground threads have ended.
· A thread's IsBackground property controls its background status,
| class PriorityTest { static void { Thread worker = new Thread(delegate() { Console.ReadLine(); }); if (args.Length > 0) { worker.IsBackground = true; } worker.Start(); } } |
| |
· Here If called with no arguments – Then the worker thread will run in the default Foreground mode - Hence will cause the wait on the ReadLine statement, waiting for the user to hit Enter, even though the main thread (Foreground thread) exits.
· Reason being – Here both the worker & main thread are Foreground threads. Thus even when the main thread (Foreground thread) exits, the other Foreground thread(here our worker thread) will still be alive, and thus wait for the user to hit Enter.
· Otherwise if an argument is passed, the worker is assigned background status, and the program exits almost immediately and will not the wait on the ReadLine statement, waiting for the user to hit.
· Reason being – As soon the main thread (Foreground thread) ends – it terminates the background thread (here our worker thread) also. Thus terminating the ReadLine statement.
Note –
When a background thread terminates in this manner, any finally blocks are circumvented. As circumventing finally code is generally undesirable, it's good practice to explicitly wait for any
background worker threads to finish before exiting an application – perhaps with a timeout (this is achieved by calling Thread.Join).
Having worker threads as background threads can then be beneficial.
Consider the alternative – foreground thread that won't die – preventing the application from exiting. An abandoned foreground worker thread is particularly dangerous with a Windows Forms application,
because the application will appear to exit when the main thread ends (at least to the user) but its process will remain running. In the Windows Task Manager, it will have disappeared from the Applications tab, although its executable filename still be visible in the Processes tab. Unless the user explicitly locates and ends the task, it will continue to consume resources and perhaps prevent a new instance of the application from starting or functioning properly.
Thread Priority
· A thread’s Priority property determines how much execution time it gets relative to other active threads in the same process, on the following scale:
enum ThreadPriority { Lowest, BelowNormal,
· This becomes relevant only when multiple threads are simultaneously active.
· Setting a thread’s priority to high doesn’t mean it can perform real-time work, because it’s still limited by the application’s process priority.
· To perform real-time work, the Process class in System.Diagnostics must also be used to elevate the process priority as follows
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
Approaches – Exception Handling:
- Any try/catch/finally blocks in scope where a thread is created are of no relevance once the thread starts executing.
- Consider the following program:
| class PriorityTest { public static void { try { new Thread(Go).Start(); } catch (Exception ex) { Console.WriteLine("Exception!"); // We'll never get here! } } static void Go() { throw null; } } |
| |
- The try/catch statement in this example is effectively useless, and the newly created thread will be encumbered with an unhandled NullReferenceException.
- The remedy is for thread entry methods to have their own exception handlers:
| class PriorityTest { public static void { new Thread(Go).Start(); } static void Go() { try { throw null; // this exception will get caught below } catch (Exception ex) { Typically log the exception, and/or signal another thread that we've come unstuck } } } |
| |
Note –
From .NET 2.0 onwards, an unhandled exception on any thread shuts down the whole application, meaning ignoring the exception is generally not an option.
Hence a try/catch block is required in every thread entry method – at least in production applications – in order to avoid unwanted application shutdown in case of an unhandled exception.
Note –
The .NET framework provides a lower-level event for global exception handling:.
This event fires when there's an unhandled exception in any thread, and in any type of application (with or without a user interface). However, while it offers a good last-resort mechanism for logging untrapped exceptions, it provides no means of preventing the application from shutting down – and no means to suppress the .NET unhandled exception dialog.
Approaches of How Threads share data:
Local Variables –
- The CLR assigns each thread its own memory stack so that local variables are kept separate.
| class PriorityTest { static void { new Thread(Go).Start(); // Call Go() on a new thread Go(); // Call Go() on the main thread } static void Go() { for (int cycles = 0; cycles < 5; cycles++) Console.Write('?'); } } Output – ???????? (10)
|
| |
Using Object Instance –
- Threads share data if they have a common reference to the same object instance. Here's an example.
| class ThreadTest { bool done; static void { ThreadTest tt = new ThreadTest(); // Create a common instance new Thread(tt.Go).Start(); tt.Go(); } // Note that Go is now an instance method void Go() { if (!done) { done = true; Console.WriteLine("Done"); } } } Output – Done (only once)
|
| |
Using Shared Data –
- Static fields offer another way to share data between threads.
| class ThreadTest { static bool done; // Static fields are shared between all threads static void { new Thread(Go).Start(); Go(); } static void Go() { if (!done) { done = true; Console.WriteLine("Done"); } } } Output – Done (only once) |
| |
Approaches of Thread Safety when Threads share data:
- Both of the above examples requires Thread safety.
- The output is actually indeterminate: it's possible (although unlikely) that "Done" could be printed twice.
- The problem is that one thread can be evaluating the if statement right as the other thread is executing the WriteLine statement – before it's had a chance to set done to true.
| static void Go() { if (!done) { done = true; Console.WriteLine("Done"); } } Output – Done Done (Usually) |
| |
Using Locking -
- The remedy is to obtain an exclusive lock while reading and writing to the common field.
- Code that's protected in such a manner – from indeterminacy in a multithreading context – is called thread-safe.
| class ThreadSafe { static bool done; static object locker = new object(); static void { new Thread(Go).Start(); Go(); } static void Go() { lock (locker) { if (!done) { done = true; Console.WriteLine("Done"); } } } } Output – Done |
| |
Using Sleep & Join -
- Temporarily pausing, or blocking, is an essential feature in coordinating, or synchronizing the activities of threads.
- Waiting for an exclusive lock is one reason for which a thread can block.
- Another is if a thread wants to pause, or Sleep for a period of time:Thread.Sleep (TimeSpan.FromSeconds (30)); // Block for 30 seconds
- A thread can also wait for another thread to end, by calling its Join method.
| Thread.Sleep (TimeSpan.FromSeconds (30)); // Block for 30 seconds |
| Thread t = new Thread (Go); // Assume Go is some static method t.Start(); Thread.Join (t); // Wait (block) until thread t ends |
Note –
A thread, while blocked, doesn't consume CPU resources.
Hope this is clear now.
Thanks & Regards,
Arun Manglick || Senior Tech Lead
No comments:
Post a Comment