![]() |
General Programming »
Threads, Processes & IPC »
Multi-threading
Beginner
License: The Code Project Open License (CPOL)
Beginners Guide To Threading In .NET Part 1 of nBy Sacha BarberBeginners Guide To Threading In .NET |
C# (C# 3.0), .NET (.NET 2.0, .NET 3.0, .NET 3.5), WPF, Dev, Design
|
||||||||||
|
Advanced Search |
|
|
|
||||||||||||||||
Im afraid to say that I am just one of those people that unless I am doing something, I am bored. So now that I finally feel I have learnt tha basics of WPF, it is time to turn my attention to other matters.
I have a long list of things that demand my attention such as (WCF/WF/CLR Via C# version 2 book), but I recently went for (and got, but turned it down in the end) which required me to know a lot about threading. Whilst I consider myself to be pretty good with threading, I thought yeah I'm ok at threading but I could always be better. So as a result of that I have decided to dedicate myself to writing a series of articles on threading in .NET. This series will undoubtely owe much to an excellent Visual Basic .NET Threading Handbook that I bought that is nicely filling the MSDN gaps for me and now you.
I suspect this topic will range from simple to medium to advanced, and it will cover a lot of stuff that will be in MSDN, but I hope to give it my own spin also.
I dont know the exact schedule, but it may end up being something like
I guess the best way is to just crack on. One note though before we start, I will be using C# and Visual Studio 2008.
What I'm going to attempt to cover in this article will be
I think thats quite enough for 1 article
When a user starts an Application, memory and a whole host of resources are allocated for the Application. The pysichal seperation of this memory and resources is called a Process. An Application may launch more than 1 Process. Its important to note that Applications and Processes are not the same thing at all.
You should all know that you can view the running Processes/Application in Windows using Task Manager.
Here is how many Application I had running
And here is the list of Processes, where it can be seen that there are many more Processes running. Applications may have one or more Processes involved, where each Process has its own seperation of data, execution code and system resources.
You might notice above that there is a reference to the CPU usage. This is down to the fact that each Process has an execution sequence used by the computers CPU. This execution sequence is known as a Thread. This Thread is defined by the registers in use on the CPU, the stack used by the Thread and a container that keeps track of the threads state (The Thread Local Storage TLS).
Creating a Process includes starting the Process running at a instruction point. This is normally known as the primary or main thread. This threads execution sequence is largely determined by how the user code is written.
With all these Processes all wanting a slice of the CPU time cycle how does it get managed. Well each Process is granted a slice of time (Quantum) on which it (the Process) may use the CPU. This slice of time should NEVER to considered a constant, it is effected by OS and CPU type.
What happens if we need our Process to do more than 1 thing, like query a web service and write to a database at the same time. Luckily we can split a Process to share the time slice allocated to it. This is done by spawning new threads in the current Process. These extra threads ares sometimes called worker threads. These worker threads share the processes memory space that is isolated from all other Processes on the system. The concept of spawning new threads within the same process is called free threading.
As some of you may (as I did) come from VB 6.0 where we had apartment threading, where each new thread was started in its own process and was granted its own data, so threads couldnt share data. Lets see some figures shall we as this is fairly important.

With this model each time you want to do some background work it happens in its own process, so was known as Out Of Process.

Free threading we can get the CPU to execut an additional thread using the same process data. This is much better than single threaded apartments, as we get all the added benifits of extra threads with the ability to share the same Process data.
NOTE : Only one thread actualy runs on the CPU at one time.
If we go back to Task Manager and change the view to include the thread count, we can see something like

This shows that each Process can clearly have more than 1 thread. So hows all this scheduling and state information managed. We will consider that next
When a threads time slice has expired it doesnt just stop and wait its turn. Recall that a CPU can only run 1 thread at a time, so the current thread needs to be replaced with the next thread to get some CPU time. Before that happens the current thread needs to store its state information to allow it to execute properly again. This is what the TLS is all about. One of the registers stored in the TLS is the program counter, which tells the thread which instruction to execute next.
Processes dont need to know about each other to be shceduled correctly. Thats really the job of the operation system, even OS have a main thread sometimes called the system thread, which schedules all other threads. It does this by using interrupts. An interrupt is a mechanism that causes the normal execution flow to branch somewhere else in the computer memory without the knowledge of the execution program.
the OS determines how much time the thread has to execute and places and places an instruction in the current threads execution sequence. Since the interrupt is within the instruction set its a software interrupt, which isnt the same as a hardware interrupt.
Interrupts are a feature used in all but the simplest microprocessors
to allow hardware devices to request attention. When an interrupt is
received, a microprocessor will temporarily suspend execution of the
code it was running and jump to a special program called an interrupt
handler. The interrupt handler will typically service the device
needing attention and then return to the previously-executing code.
One
of the interrupts in all modern computers is controlled by a timer,
whose function is to demand attention at periodic intervals. The
handler will typically bump some counters, see if anything interesting
is supposed to happen, and if there's nothing interesting (yet),
return. Under Windows, one of the 'interesting' things that can happen
is the expiry of a thread's time slice. When that occurs, Windows will
force execution to resume in a different thread from the one that was
interrupted.
Once an interrupt is placed the OS then allows the thread to execute. When the thread comes to the interrupt the OS uses a special function called an interrupt handler to store the threads state in the TLS. Once the thread time slice has timed out, it is moved to the end of the thread queue for its given priority (more on this later) to wait its turn again.

This is ok if the thread isn't done or needs to continue executing. What happens if the thread decides it does need any more CPU time just yet (maybe wait for a resource), so yeild its time slice to another thread.
This is down to the programmer and the OS. The programmer does the yield (normally using Sleep() method) the thread then clears any interrupts that teh OS may have placed in its stack. A software interrupt is then simulated. The thread is stored in the TLS and moved to the end of the queue as before.
The OS may have however already placed an interrupt in the threads stack, which
must be cleared before the thread is packed away, otherwise when it executes
again, it may get interrupted before it should be. The OS does this (thank goodness).
Thread Sleep And Clock Interrupts
As we just said a thread may decide to yield its CPU time to wait for a resource, but this could be 10 or 20 minutes, so the programmer may choose to make the thread sleep, which results in the thread being packed in the TLS. But it doesnt go to the runnable queue it goes to a sleep queue. In order for threads in the sleep queue to run again they need a different kind of interrupt, called a clock interrupt. When a thread enters the sleep queue a new clock interrupt is scheduled for the time that the thread when the thread should be awoken. When a clock interrupt occurs that matches an entry on the sleep queue the thread is moved back to the runnable queue.

Thread Abort / Thread done
All things have an end. When a thread is finished or it is programmatically aborted, the TLS for that thread is de-allocated. The data in the Process remains (remember its shared between all the Process threads, there could be more than 1), and will only be de-allocated when the Process itself is stopped.
So weve talked a bit about scheduling, but we also said the TLS stored state for threads, how does it do this. Well consider the following from MSDN
"Threads use a local store memory mechanism to store thread-specific data. The common language runtime allocates a multi-slot data store array to each process when it is created. The thread can allocate a data slot in the data store, store and retrieve a data value in the slot, and free the slot for reuse after the thread expires. Data slots are unique per thread. No other thread (not even a child thread) can get that data.
If the named slot does not exist, a new slot is allocated. Named data slots are public and can be manipulated by anyone."
Thats how in a nutshell. Lets see the MSDN example (Blatant steal here)
using System;
using System.Threading;
namespace TLSDataSlot
{
class Program
{
static void Main()
{
Thread[] newThreads = new Thread[4];
for (int i = 0; i < newThreads.Length; i++)
{
newThreads[i] =
new Thread(new ThreadStart(Slot.SlotTest));
newThreads[i].Start();
}
}
}
class Slot
{
static Random randomGenerator = new Random();
public static void SlotTest()
{
// Set different data in each thread's data slot.
Thread.SetData(
Thread.GetNamedDataSlot("Random"),
randomGenerator.Next(1, 200));
// Write the data from each thread's data slot.
Console.WriteLine("Data in thread_{0}'s data slot: {1,3}",
AppDomain.GetCurrentThreadId().ToString(),
Thread.GetData(
Thread.GetNamedDataSlot("Random")).ToString());
// Allow other threads time to execute SetData to show
// that a thread's data slot is unique to the thread.
Thread.Sleep(1000);
Console.WriteLine("Data in thread_{0}'s data slot is still: {1,3}",
AppDomain.GetCurrentThreadId().ToString(),
Thread.GetData(
Thread.GetNamedDataSlot("Random")).ToString());
// Allow time for other threads to show their data,
// then demonstrate that any code a thread executes
// has access to the thread's named data slot.
Thread.Sleep(1000);
Other o = new Other();
o.ShowSlotData();
Console.ReadLine();
}
}
public class Other
{
public void ShowSlotData()
{
// This method has no access to the data in the Slot
// class, but when executed by a thread it can obtain
// the thread's data from a named slot.
Console.WriteLine(
"Other code displays data in thread_{0}'s data slot: {1,3}",
AppDomain.GetCurrentThreadId().ToString(),
Thread.GetData(
Thread.GetNamedDataSlot("Random")).ToString());
}
}
}
This may produce the following

It can be seen that this uses 2 things
GetNamedDataSlot : looks up a named slotSetData : Sets the data in the specified slot within the current threadThere is another way, we can also use the ThreadStaticAttribute
which means the value is unique for each thread. Lets see the MSDN example (Blatant
steal here)
using System;
using System.Threading;
namespace ThreadStatic
{
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 3; i++)
{
Thread newThread = new Thread(ThreadData.ThreadStaticDemo);
newThread.Start();
}
}
}
class ThreadData
{
[ThreadStaticAttribute]
static int threadSpecificData;
public static void ThreadStaticDemo()
{
// Store the managed thread id for each thread in the static
// variable.
threadSpecificData = Thread.CurrentThread.ManagedThreadId;
// Allow other threads time to execute the same code, to show
// that the static data is unique to each thread.
Thread.Sleep(1000);
// Display the static data.
Console.WriteLine("Data for managed thread {0}: {1}",
Thread.CurrentThread.ManagedThreadId, threadSpecificData);
}
}
}
And this may produce the following output

When I talked about Processes earlier, I mentioned that Processes have physically
isolated memory and resources needed to maintain themselves, and I also mentioned
that a Process has at least 1 thread. Microsoft also introduced 1 extra layer
of abstraction/isolation called an AppDomain. The AppDomain
is not a physical isolation, but rather a logic isolation within the Process.
Since more than 1 AppDomain can exist in Process we get some benifits.
For example until we had an AppDomain Processes that needed to
access each others data had to use a Proxy, which introduced extra code and
overhead. By using a AppDomain it is possible to launch several
applications within the same Process. The same sort of isolation that exists
with Processes is also available for AppDomain.Threads can execure
across application domains without the overhead of inter process communication.
This is all encapsulated within the AppDomain class. Any time a
namespace is loaded in an application it is loaded into an AppDomain.
The AppDomain used will be the same as the calling code unless
otherwise specified. AppDomain may or may not contain threads,
which is different to Processes.
As I stated above AppDomains are a further level of abstraction/isolation, and they sit within a Process. So why use AppDomains? One of this articles readers actually gave a very good example to this question.
"I have previously needed to execute code in a separate AppDomain for a Visual Studio AddIn that used reflection to look at the current project's DLL file. Without examining the DLL in a separate AppDomain, any changes to the project made by the developer would not show up in reflection unless they restarted Visual Studio. This is exactly because of the reason pointed out by Marc: once an AppDomain loads an assembly, it can't be unloaded."
AppDomain forum post, by Daniel Flowers
So we can see that an AppDomain can be use to load and Assembly dynamically, and the entire AppDomain can be destroyed, without affecting the Process. I think this illustrates the abtraction/isolation that an AppDomain gives us.
NUnit also takes this approach, but more on this below.
Lets see an example of how to work with AppDomain data
using System;
using System.Threading;
namespace AppDomainData
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Fetching current Domain");
//use current AppDomain, and store some data
AppDomain domain = System.AppDomain.CurrentDomain;
Console.WriteLine("Setting AppDomain Data");
string name = "MyData";
string value = "Some data to store";
domain.SetData(name, value);
Console.WriteLine("Fetching Domain Data");
Console.WriteLine("The data found for key {0} is {1}",
name, domain.GetData(name));
Console.ReadLine();
}
}
}
This produces the rather unexciting output

And how about executing code in a specific AppDomain, let see
that now
using System;
using System.Threading;
namespace LoadNewAppDomain
{
class Program
{
static void Main(string[] args)
{
AppDomain domainA = AppDomain.CreateDomain("MyDomainA");
AppDomain domainB = AppDomain.CreateDomain("MyDomainB");
domainA.SetData("DomainKey", "Domain A value");
domainB.SetData("DomainKey", "Domain B value");
OutputCall();
domainA.DoCallBack(OutputCall); //CrossAppDomainDelegate call
domainB.DoCallBack(OutputCall); //CrossAppDomainDelegate call
Console.ReadLine();
}
public static void OutputCall()
{
AppDomain domain = AppDomain.CurrentDomain;
Console.WriteLine("the value {0} was found in {1}, running on thread Id {2}",
domain.GetData("DomainKey"),domain.FriendlyName,
Thread.CurrentThread.ManagedThreadId.ToString());
}
}
}

Since I first published this article there has been a few suggestions, the one that seemed to get the most attention (at least for this articles content) was NUnit and AppDomains, so I thought I better address that.
Shown below are to interesting quotes that I found on the NUnit site and also a personal blog.
"Dynamic reloading of an assembly using AppDomains and shadow copying. This also applies if you add or change tests. The assembly will be reloaded and the display will be updated automatically. The shadow copies use we use a configurable directory specified in the executable’s (nunit-gui and nunit-console) config files."
"NUnit was written by .NET Framework experts. If you look at the NUnit source, you see that they knew how to dynamically create AppDomains and load assemblies into these domains. Why is a dynamic AppDomain important? What the dynamic AppDomain lets NUnit do is to leave NUnit open, while permitting you to compile, test, modify, recompile, and retest code without ever shutting down. You can do this because NUnit shadow copies your assemblies, loads them into a dynamic domain, and uses a file watcher to see if you change them. If you do change your assemblies, then NUnit dumps the dynamic AppDomain, recopies the files, creates a new AppDomain, and is ready to go again."
Essentially what NUnit does is host the test Assembly in a seperate AppDomain. And as AppDomains are isolated, they can be unloaded without affecting the Process to which they belong.
Just as in real life we as human have priorities, so to do Threads. A programmer can decide a priority for their Thread, but ultimately its up the recipient to decide what should be acted upon now, and what can wait.
Windows uses a priority system from 0-31 where 31 is the higest. Anything higher than 15 needs to be done via an Administrator. Threads that have priority between 16-31 are considered real time and will pre-empt lower priority level threads. Think about drivers/input devices and things like this, these will be running with priorities between 16-31.
In Windows there is a sceduling system (typically round robin) where each priority has a queue of threads. ALL threads with the highest priority are allocated some CPU time, then the next level (lower down) are allocated some time and so on. If a new thread appears with a higher priority then the current thread is pre-empted and the new higher level priority level thread is run. Lower level priority threads will only be scheduled if there are no higher level threads in other priority queues.
If we again use Task Manager we can see that it is possible to alter a Process to have a higher priority that will enable any new spawned threads a higher possibility of being scheduled (given some CPU time).
But we also have options when we use code, as the System.Threading.Thread class exposes a Priority property. If we look at what MSDN states, we could set on of the following:
A thread can be assigned any one of the following priority values:
NOTE : Operating systems are not required to honor the priority of a thread.
For instance, a OS may decay the priority assigned to a high-priority thread, or otherwise dynamically adjust the priority in the interest of fairness to other threads in the system. A high-priority thread can, as a consequence, be preempted by threads of lower priority. In addition, most OSs have unbounded dispatch latencies: the more threads in the system, the longer it takes for the OS to schedule a thread for execution. Any one of these factors can cause a high-priority thread to miss its deadlines, even on a fast CPU.
The same could be said for programatically setting a user created Thread to be set at a Highest level priority. So be warned, be careful what you do when setting priorities for Threads.
Starting new Threads is pretty easy, we just need to use one of the Thread constructors, such as
There are others, but these are the most common ways to start threads. Lets look at an example of each of these
No Parameters
Thread workerThread = new Thread(StartThread);
Console.WriteLine("Main Thread Id {0}",
Thread.CurrentThread.ManagedThreadId.ToString());
workerThread.Start();
....
....
public static void StartThread()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Thread value {0} running on Thread Id {1}",
i.ToString(),
Thread.CurrentThread.ManagedThreadId.ToString());
}
}
Single Parameter
//using parameter
Thread workerThread2 = new Thread(ParameterizedStartThread);
// the answer to life the universe and everything, 42 obviously
workerThread2.Start(42);
Console.ReadLine();
....
....
public static void ParameterizedStartThread(object value)
{
Console.WriteLine("Thread passed value {0} running on Thread Id {1}",
value.ToString(),
Thread.CurrentThread.ManagedThreadId.ToString());
}
Putting it all together we can see a small program with a main thread and 2 worker threads.
using System;
using System.Threading;
namespace StartingThreads
{
class Program
{
static void Main(string[] args)
{
//no parameters
Thread workerThread = new Thread(StartThread);
Console.WriteLine("Main Thread Id {0}",
Thread.CurrentThread.ManagedThreadId.ToString());
workerThread.Start();
//using parameter
Thread workerThread2 = new Thread(ParameterizedStartThread);
// the answer to life the universe and everything, 42 obviously
workerThread2.Start(42);
Console.ReadLine();
}
public static void StartThread()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Thread value {0} running on Thread Id {1}",
i.ToString(),
Thread.CurrentThread.ManagedThreadId.ToString());
}
}
public static void ParameterizedStartThread(object value)
{
Console.WriteLine("Thread passed value {0} running on Thread Id {1}",
value.ToString(),
Thread.CurrentThread.ManagedThreadId.ToString());
}
}
}
Which may produce something like

Pretty simpe huh.
So you have now seen some simple examples of creating threads.
What we have not seen yet is the principle of Synchronization between threads.
Threads run out of sequence from the rest of the application code, so you can never be sure of the exact order of events. That is to say, we can't garuantee that actions that effect a shared resources used by one thread, will be completed before code is run in another thread.
We will be looking at this in great detail in subsequent articles, but for
now let's consider a small example using a Timer. Using a Timer
we can specify that a method is called at some interval and this could check
the state of some data before continuing. Its a very simply model, the next
articles will show more detail about more advanced Synchronization techniques,
but for now lets just use a Timer.
Lets see a very small example. This example starts a worker thread, and a Timer.
The main thread is put in a loop waiting for a completed flag to be set "true".
The Timer is waiting for a message from the worker thread of "Completed"
before allowing the blocked main thread to continue by setting the completed
flag to "true".
using System;
using System.Threading;
namespace CallBacks
{
class Program
{
private string message;
private static Timer timer;
private static bool complete;
static void Main(string[] args)
{
Program p = new Program();
Thread workerThread = new Thread(p.DoSomeWork);
workerThread.Start();
//create timer with callback
TimerCallback timerCallBack =
new TimerCallback(p.GetState);
timer = new Timer(timerCallBack, null,
TimeSpan.Zero, TimeSpan.FromSeconds(2));
//wait for worker to complete
do
{
//simply wait, do nothing
} while (!complete);
Console.WriteLine("exiting main thread");
Console.ReadLine();
}
public void GetState(Object state)
{
//not done so return
if (message == string.Empty) return;
Console.WriteLine("Worker is {0}", message);
//is other thread completed yet, if so signal main
//thread to stop waiting
if (message == "Completed")
{
timer.Dispose();
complete = true;
}
}
public void DoSomeWork()
{
message = "processing";
//simulate doing some work
Thread.Sleep(3000);
message = "Completed";
}
}
}
This may produce something like

Well thats all I wanted to say this time. Threading is a complex subject, and as such this series will be quite hard, but I think worth a read.
Next time we will be looking at Lifecycle Of Threads/Threading Opportunities/Traps.
Could I just ask, if you liked this article could you please vote for it, as that will tell me whether this Threading crusade that I am about to embark on will be worth creating articles for.
I thank you very much
v1.1 19/05/08 : Added why to use AppDomain/NUnit with AppDomain, and extra parts to threading priorities
v1.0 18/05/08 : Initial issue
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 9 Aug 2008 Editor: |
Copyright 2008 by Sacha Barber Everything else Copyright © CodeProject, 1999-2009 Web10 | Advertise on the Code Project |