Manual Threading in ASP.NET 2.0
Recall That ..
In Multithreading in ASP.NET tutorial we discussed multithreading, what is it, when to use it, what is the advantages and
disadvantages of using it, and finally we presented the thread pool technique
that you can use to develop a multithreaded application in an easy way.
Some times this easy way is not sufficient to manage certain cases within your application, and the need for manually creating and
fully managing a new thread arises. Creating threads manually is much more complex than using
the thread pool, and requires you to have knowledge about threading life
cycle and concepts.
In this tutorial, we will explore the world
of multithreading, we will understand how to manually create, manage, and
synchronize threads.
Some Basic Concepts about Multithreading
You may already know that any operating system such as Windows
or whatever, uses processes to separate the applications running on the machine
in the same time. Each process by default starts with one thread. Operating
systems can allocate run time per thread, not per process, or per
application. Each thread has its time slice to execute in. Each process can
have more than one thread.
The .NET framework further subdivides each operating
system process into managed sub processes called: Application Domains,
represented by the "System.AppDomain" namespace. Each application domain can
have one
or more threads, and we will called them managed threads (represented by the "System.Threading.Thread"
namespace). Each managed thread can run in one or any number of application
domains within the managed process.
Dealing with Managed Threads
You can deal with a managed thread through the "System.Threading.Thread"
class, which gives you the ability to create, and control a thread, to set its
priority, and to get its status.
As you know, a process can create one or more thread to
execute a portion of the program code associated with that process. During its
life cycle, a thread is always in one state of the states defined by the Thread State enumeration.
Examples of these states are: "Running", "Stopped",
"Aborted", "Suspended", and so on. You can assign a priority level to your
thread by using the "ThreadPriority" enumeration, like; "Normal", "AboveNormal",
"Highest", and so on. You also can get your thread identification number by
calling the "GetHashCode" method.
Creating Managed Threads
When an application is running for the first time, a new
process is created, and the operating system injects a thread to execute code in
this process. We call this thread the main application thread. You can get
this thread or the current running thread by using the "Thread.CurrentThread"
property.
To explore this, let's create a new web application from
"File / New / Web Site". In the "Default.aspx" design view, draw a label control.
Double click the page area to activate the "Page_Load" event handler. Import the
"System.Threading" name space as shown below (Our full application for
this part of the tutorial can be downloaded here).
1 Imports System.Threading
Then write the following lines of code.
5 Protected Sub Page_Load(ByVal sender As Object, _
6 ByVal e As System.EventArgs) Handles Me.Load
7
8 GetCurrentThreadInfo(Thread.CurrentThread)
9
10 End Sub
11 Public Sub GetCurrentThreadInfo(ByVal MThread As Thread)
12 If MThread.IsThreadPoolThread Then
13 Me.Label1.Text = "Belongs to the Managed Thread Pool"
14 Else
15 Me.Label1.Text = "Does Not Belong to the Managed Thread Pool"
16 End If
17
18 Dim i As Integer = MThread.GetHashCode
19 Label1.Text = Label1.Text + " || ID = " + i.ToString
20
21 Label1.Text = Label1.Text + " || Priority = " _
22 + MThread.Priority.ToString()
23
24 Label1.Text = Label1.Text + " || State = " _
25 + MThread.ThreadState.ToString()
26 End Sub
Run your application by pressing F5, and you will get a result
similar to the following result.
 Figure 1 -
Output
showing the current thread information
Now, type the following lines of code to create a new
managed thread:
4 dim T as Thread
5 Protected Sub Page_Load(ByVal sender As Object, _
6 ByVal e As System.EventArgs) Handles Me.Load
7
8 ' GetCurrentThreadInfo(Thread.CurrentThread)
9
10 '-- Create a new thread
11
12 T = New Thread(AddressOf LongTimeTask)
13 T.Start()
14 GetCurrentThreadInfo(T)
15
16 End Sub
Now add the "LongTimeTask" method as
follows:
33 Public Sub LongTimeTask()
34
35 'suspend the current thread for specified time.
36 Thread.Sleep(10000)
37 MsgBox("ok2")
38 End Sub
From the above lines of code you can understand that to create
a new thread all you need is to define a new thread instance and then giving it the
reference of the method you want to run in a separate thread, then you will need
to call the
"Start" method of the thread. When you call this method, the new thread
will be started, and the call will be returned immediately to the caller thread.
The "GetCurrentThreadInfo" at line #14 will show information of our newly created
and started thread.
Run your application now, and you will get a result
similar to the following:
 Figure 2 -
Output
showing the new thread information
Pause and Resume
In some situations you may want to pause a running thread
for a certain period of time, to do this use "Thread.sleep(x)", where
x is the
period of pause time in milliseconds.
You can also pause a thread by calling "Thread.suspend",
and when you want to resume it again then just call "Thread.Resume" from within the working
thread.
You can also use "Thread.Join" to block the
caller thread till the called thread finishes. Add the following lines of code in bold face to
our "Page_Load" method to test this join method.
5 Protected Sub Page_Load(ByVal sender As Object, _
6 ByVal e As System.EventArgs) Handles Me.Load
7
8 ' GetCurrentThreadInfo(Thread.CurrentThread)
9
10 '-- Create a new thread
11
12 T = New Thread(AddressOf LongTimeTask)
13 T.Start()
14 T.Join()
15 Label1.Text = "ok1"
16 ' GetCurrentThreadInfo(T)
17
18 End Sub
Now Run the application and you will get after 10 seconds a
message box that says "ok2", and after clicking it you will see "ok1" written in your
label. This means that, the main worker thread that calls the "join" method
stop execution till the new "T" thread finished executing its code, and
then the main thread resumes its work again.
Abort
You can destroy (and obviously stop) a thread by calling "Thread.Abort"
method. When you call this method a "ThreadAbortException" will be thrown.
The thread can catch it and do whatever finalization in the associated "Finally" block of
a "Try
.. Catch ..Finally" block of code. Because that "Thread.Abort" does not
terminate the thread immediately, you can call the "Thread.Join" to wait till
the called thread is fully terminated. Once a thread is terminated or in other words
aborted, you can not restarted it again.
Synchronization
Assume that your application has two threads, and these
two threads are performing calls to properties or methods of a single object, or may be
to a
certain piece of code into your application. Assume that the first thread
changes one of
this object's properties and while it is changing it, the other thread attempt
to perform a change in the same property. The object in this case is not in a
valid state, and this can crash your application! The problem is that you will
not be certain of what is the value you will get, is it the old one, or the new one,
or you will get an error? This is an invalid state that must be avoided,
and this is what synchronization does. If an object is built to avoid such a
problem then, we call this object 'thread
safe'.
Synchronization is about organizing or let us say
serializing the access to shared resources. This is performed in such a way that only one thread
at a time is allowed to access these resources. To be able to do this, .NET
framework provides four categories of synchronization schemes, they are:
-
No Synchronization
-
Synchronized Context
-
Synchronized Code Region
-
Manual Synchronization
No Synchronization
This is the default, any thread can access any field or
method at any time.
Synchronized Context
This is a simple automatic way you can use to make any
object of type "ContextBoundObject" thread safe by using the "SynchronizationAttribute"
object. This means that any field or method of this "ContextBoundObject"
object can be accessed by only one thread at any time, and this will be done
automatically without writing any extra code from your side.
Synchronized Code Region
Under this category you can make use of some objects
provided by the .NET framework to serialize access to a critical code region, or
a shared object. By using the "Monitor" object you can serialize access to a shared
object. It is just like a lock, that when one thread has, the other can not
access this object until this lock is released. You can use "Monitor.Enter"
to raise this lock, and "Monitor.Exit" to unlock.
You can also use "SyncLock", or "Lock" to do the same but
for lines of code not for objects. To mark some block of code as a thread
safe block, use "SyncLock" / "End SyncLock" statement in visual basic, or "Lock
/ End Lock" in C#.
By using these objects, all what you have to do is to
mark your object or piece of code as a thread safe, and the common language
runtime will do the rest for you. It will carry the whole serialization process
behind the scenes.
Manual Synchronization
In manual synchronization, you can use many types of
object to define your own synchronization mechanism. Examples of these objects
are; "ReaderWriterLock", "AutoResetEvent", "ManualResetEvent", and "Mutex".
Deadlocks
Deadlock is a famous problem you can avoid simply by
careful programming. To first explain what is deadlock, let us assume that we
have two threads, thread 1, and thread 2. Let us assume, as well, that we have two
objects, object x, and object y. If thread 1 holds a lock on object x and waits
for a lock on object y, and thread 2 holds a lock on object y, and waits for a
lock on object x then threads will be waiting forever! and this is what we call
a deadlock situation.
To solve this, use methods for synchronization
that provides a timeout ability. You can for example use "Monitor.TryEnter"
with a timeout parameter, to try to obtain a lock on a certain object within a
certain time interval.
Race Conditions
Race condition is a bug in your application, occurs when
the result of your application depends on which one of two or more threads
reaches a shared block of code first. In this case, the application output
changes each time it is executed!
As an example; assume that we have a shared integer
object called x, and we have two threads 1, and 2. Thread number 1 attempt to
increment the x object by one, and during this increment process, its time slice
has been finished. Thread 2 time slice just start and it attempt to increment
the same x object too. Thread 2 incremented the x object successfully, and then
its time slice finished. Thread 1 starts a new time slice and completing the
increment process not knowing that the object x value is already changed. This
is a race condition, and the output of such code is of course incorrect!
The above race condition problem can be solved by using
an object like "InterLock", with its "Increment", and "Decrement" methods.
Race conditions can be avoided generally by considering
each line of code you write, and asking yourself: What might happen if a thread
finished before executing this line? or during executing this line? and another
thread overtook it?
|