摘至:
http://www.codeproject.com/KB/threads/Threading.aspx
Introduction
This article is to guide you through creating and handling threads in .NET. It has two sections: in the first section, we will see about threading basics and how to create threads in .NET using C#. In the second section, we will see how to handle threads in WinForms.
1.1 Threading
1.1.1 Threading Basics
If you are familiar with threading basics, you can skip this topic.
A thread can be defined as a piece of code in execution. Each process has no less than one thread. So, all the processes get executed in terms of a thread. Any application will have an entry point called Main, and at the maximum, it can have only one Main. This is where an application starts. The thread which executes this Main function (i.e., starts the application) is called the main thread. This main thread can spawn any number of threads, depending upon the application requirement, to execute functionalities in parallel. Multiple threads in an application enable parallel execution. The term multiple threads signifies the total number of threads within a single process, not across different processes.
Multiple threads in execution is not similar to multiple process in execution. Each process has its own address space. The address space of one process cannot be used (read/write) by another process. Any communication between the processes should happen through some IPC (Inter Process Communication) mechanisms. But, multiple threads in a process share the process address space. They don't need any special mechanism for communication. They can communicate each other directly using a variable or some signals. Also, context-switching (this is how we achieve parallel execution using a single processor!!!) is time consuming to switch between processes, but not in the case of switching between threads. So, multiple threads enable parallel execution within a process effectively.
Do I really need multithreading?
Consider you have a calculator application which does some complex arithmetic operations. And, you need to provide a stop feature so that the user can stop the calculation at the middle, before letting it to finish. Having only one thread in your application is not possible to address this requirement. You can not process the stop function while you are doing an arithmetic operation. As the application will be busy with executing the arithmetic operation, it cannot handle the stop until the arithmetic operation is completed. Here, you need a thread which can execute the complex arithmetic operation, and another (the main thread) will listen to user inputs, in this case, the stop. When the main thread receives the stop signal, it can stop the thread spawned for execution of the arithmetic operation.
You have to be more careful when creating threads, because creating unwanted threads may reduce your application performance. Similarly, avoiding threads in required places will distress your application.
1.1.2 Threading in .NET
If you are familiar with delegates, you can skip this topic.
Before starting with threads, let's understand what delegates are. A delegate can be defined as a data type which holds an array of methods. All the methods inside a delegate should have the same signature and the same return type. The method signature and the return type of a delegate will be defined in the delegate declaration itself. The C# code for a delegate declaration is:
Collapse Copy Code
public delegate void MyDelegate(int arg);
Here, the delegate MyDelegate
can hold methods with the signature void functionname(int k)
.
To add a function into a delegate, create the instance of the delegate and pass the function name as the argument.
Collapse Copy Code
MyDelegate delInstance = new MyDelegate(Function1);
Multiple functions can also be added in a delegate; to add more functions, use the +=
operator.
Collapse Copy Code
delInstance += new MyDelegate(Function2);
Here, Funtion1
and Function2
have the same signature as defined in the MyDelegate
declaration. I.e., they should return void
and take a parameter of type int
.
Delegates can be invoked as normal functions, just by passing the arguments.
Collapse Copy Code
delInstance(5);
The above line of code will invoke all the functions inside the delInstance
delegate, sequentially. First, Function1(5)
will be called, and then Function2(5)
. This is called multicasting delegates, i.e., a single invoke of the delegate instance in turn calls multiple functions inside the delegate.
1.1.3 Creating Threads in .NET
Threads in .NET are created using the class "Thread
". This class is available in the "System.Threading
" namespace. To create a new thread, you need to create an instance of the Thread
class and apply the Start
method (available in the Thread
class) on that instance. The Thread
class constructor takes a ThreadStart
delegate as a parameter. (ThreadStart
is a delegate defined in the System.Threading
namespace.) The signature of the ThreadStart
delegate takes no arguments and returns void
. So, the function to be executed in a separate thread should return void
, and should not take any arguments.
The C# code for creating threads:
Collapse Copy Code
public static void main()
{
Thread t = new Thread(new ThreadStart(ClassName.MyFunction));
t.Start();
for(int i=0;i<100;i++)
Console.WriteLine("I am in Main Thread {0}",i);
}
public static void MyFunction()
{
for(int i=0;i<100;i++)
Console.WriteLine("I am in Different Thread {0}",i);
}
In the above code, we defined a function MyFunction
which matches the ThreadStart
delegate signature. Then, we created an instance for the Thread
class, passing the ThreadStart
delegate instance. Then, we started a separate thread to execute the MyFunction
while the main thread which spawned the MyFunction
in a separate thread was also running.
On executing this, we get the console messages swapped between the main thread and a different thread since both the Main
and MyFunction
'for
' loops are running in parallel.
This is how we create threads in .NET. Here, the Thread
class can not take delegates of any signature as parameter, it just takes the ThreadStart
delegate type as parameter, and the ThreadStart
delegate can take only a function having no arguments and no return type.
Then, what if you want to execute a function in a thread which takes some argument? The answer for this question can be delegates.
Delegates have an inbuilt function called BeginInvoke()
provided by the compiler. This function can be used for executing a function in a thread. BeginInvoke
calls on any delegate will be executed in a separate thread.
The C# code follows:
Collapse Copy Code
public void MyDelegate(int i, string str);
public static void main()
{
MyDelegate delInstance = new MyDelegate (MyFunction);
delInstance.BeginInvoke(100," I am in Delegate Thread", null, null);
for(int i=0;i<100;i++)
Console.WriteLine("I am in Main Thread {0}",i);
}
public static void MyFunction(int count, string str)
{
for(int i=0;i<count;i++)
Console.WriteLine(str + " {0}",i);
}
In the above code, the BeginInvoke
call on the delegate instance runs the MyFunction
in a separate thread. The output of the above code will be, console messages swapped between the main thread and the delegate thread (similar to using the Thread
class). Note that multicasting a delegate through BeginInvoke
is not possible.
Here, our MyFunction
returns no value. What if the function returns some value? How do we collect the return value?
As BeginInvoke
is an asynchronous call (just triggers the other thread and keeps running the next line, not waiting to finish the other thread), it cannot get the return value of the function call directly. So, we need some mechanism to collect the return value. The implementation of the BeginInvoke
function in C# returns an IAsyncResult
type. This IAsyncResult
can be used for getting the return value, when required. There is a function called EndInvoke
, which takes this IAsynResult
as argument and collects the return value. So, the EndInvoke
function can be used to collect the return value.
Every EndInvoke
should be associated with a BeginInvoke
as EndInvoke
operates on the return value of BeginInvoke
.
The return value can be collected with the following C# code:
Collapse Copy Code
public bool MyDelegate(int i, string str);
public static void main()
{
MyDelegate delInstance = new MyDelegate (MyFunction);
IAsyncResult ref = delInstance.BeginInvoke(100,
"I am in Delegate Thread", null, null);
for(int i=0;i<100;i++)
Console.WriteLine("I am in Main Thread {0}",i);
bool status = delInstance.EndInvoke(ref);
}
public static bool MyFunction(int count, string str)
{
for(int i=0;i<count;i++)
Console.WriteLine(str + " {0}",i);
return true;
}
In the above code, the call to the EndInvoke
function takes the IsyncResult
type returned by the BeginInvoke
, and returns the value returned by the MyFunction
. Thus, the EndInvoke
can be used to obtain the result of BeginInvoke
.
Consider the following program in C#:
Collapse Copy Code
public void MyDelegate(int i, string str);
public static void main()
{
MyDelegate delInstance = new MyDelegate (MyFunction);
delInstance.BeginInvoke(1000, "I am in Delegate Thread", null, null);
}
public static void MyFunction(int count, string str)
{
for(int i=0;i<count;i++)
Console.WriteLine(str + " {0}",i);
}
If we look at the program closely, the main thread finishes its execution before the other thread is finished. When the main thread is destroyed, all the other threads automatically get destroyed. So, you may not get the desired output. To avoid this problem, we can use EndInvoke
. The EndInvoke
function can also be used for blocking the current thread until the corresponding thread finishes its execution. A call to EndInvoke
will block the current thread.
The C# code using EndInvoke
to block the thread follows:
Collapse Copy Code
public void MyDelegate(int i, string str);
public static void main()
{
MyDelegate delInstance = new MyDelegate (MyFunction);
ISyncResult ref = delInstance.BeginInvoke(100," I am in Delegate Thread",
null, null);
delInstance.EndInvoke(ref);
}
public static void MyFunction(int count, string str)
{
for(int i=0;i<count;i++)
Console.WriteLine(str + " {0}",i);
}
In the above code, the call to the EndInvoke
blocks the main thread until the MyFunction
is completed.
A call to EndInvoke
will collect the return value immediately if the thread has completed its execution already; otherwise, it will wait until the thread is completed and gets the result.
While making the call to BeginInvoke
, we have not created any thread, then who is creating the thread? How does it run in a separate thread?
To answer these questions, let's first know what thread pools are.
1.1.4 Thread Pool
Creating a thread at runtime is not an easy job. Most of the time will be spent on creating the thread rather than executing the functionality on the thread, in the case of smaller threads.
The CLR (Common Language Runtime) has some threads created already in a pool so that they can be used whenever required. These threads are called Thread Pool threads. If we are using a thread in the thread pool, on completion of a thread, the thread will not be destroyed. The thread will return to the thread pool in suspended state. This suspended thread can be reused for other purposes.
The BeginInvoke
function uses these Thread Pool threads to execute functionalities. The usage of thread pool threads saves the thread creation time.
1.2 Threading in WinForms
In section 1.1, we saw how to create a thread using the Thread
class and delegates. In this section, let us see how to handle threads in WinForms. (This article assumes that you have worked on WinForms applications.)
1.2.1 WinForms Internals
Before starting with the threads directly, let's understand how a Windows application works. Consider you have a Windows application where you create a text box and add the text box to the form. The C# code follows:
Collapse Copy Code
Form1_Load(……)
{
AddControl();
}
Void AddControl()
{
TextBox textBox1 = new TextBox();
Controls.add(textBox1);
}
On loading the form, the AddControl
function is invoked, and it creates a text box and adds it to the form using the Controls.Add
method.
The above code will run without any errors.
Consider the same application, where we would like to call the AddControl()
method in a thread. The C# code follows:
Collapse Copy Code
Form1_Load(……)
{
Thread t = new Thread(new ThreadStart(RunInThread));
t.Start();
}
Void RunInThread()
{
AddControl();
}
Void AddControl()
{
TextBox textBox1 = new TextBox();
controls.add(textBox1);
}
There is no change, except we call the function AddControl
from a separate thread (not from the main thread). Here, the functions RunInThread
and AddControl
run in the same thread. If you try to run the above code, though the code will compile without any errors, you will receive a runtime error saying: "Controls created on one thread cannot be parented to a control on a different thread." This is what we should worry about when dealing with threads in WinForms.
So, why do we get this runtime error? A rule in Windows is that "a control created on one thread cannot be modified in another thread". In the second example, the form was created in the main thread, and we were trying to modify the form object from another thread. That is a violation of the Windows rule (why do we have the rule is a different topic to explain). It throws the runtime error as we tried to update the control from a different thread. We can simply say that any update on the UI should happen only through the main thread.
Then, how do we update the UI while we are in a different thread? Before looking at the answer, let us understand how Windows applications work.
All Windows applications have two queue structures, the send message queue and the post message queue. These queues will have actions to be performed. The main thread is responsible for executing all the tasks in the queue. The main thread will dequeue the messages one by one and executes them.
- Post Message Queue
- Send Message Queue
- Main Thread
The post message queue will have messages posted through the PostMessage
function. Similarly, the send message queue will have messages posted by the SendMessage
function. The difference between SendMessage
and PostMessage
is that SendMessage
will block the call until it is executed. But PostMessage
just puts the message in the queue and returns. The main thread first executes the send message queue messages and then it starts executing the post message queue messages.
So, the answer for our question on how to update the UI while we are in different thread is to put it in the message queue. The next question is how to post/send a message in .NET?
Answering this question is very simple. A Control
class has three methods:
Invoke
BeginInvoke
EndInvoke
The Invoke
method does SendMessage
. The BeginInvoke
method does PostMessage
. The EndInvoke
method is used as part of BeginInvoke
to obtain the results of the BeginInvoke
call (as discussed in section 1.1).
1.2.2 Control.Invoke
As mentioned earlier, the Invoke
method on the Control
class places a calls into the send message queue. The call will ultimately be executed by the main thread, and the Invoke
method will block the current thread until the call is returned.
The Invoke
method on the Control
class has two overloads:
public System.Object Invoke ( System.Delegate method )
public virtual new System.Object Invoke ( System.Delegate method , object[] args )
The argument takes the delegate to be executed on the Main thread. If the parameters need to be passed for the delegate, they can be passed as an object
array. The return value can also be obtained as an object
.
Now, let's go back to the program:
Collapse Copy Code
public delegate void MyDelegate();
Form_Load(……)
{
Thread t = new Thread(new ThreadStart(RunInThread));
t.Start();
}
void RunInThread ()
{
MyDelagate delInstatnce = new MyDelagate(AddControl);
this.Invoke(delInstatnce);
MessageBox.Show("Hello");
}
void AddControl()
{
TextBox textBox1 = new TextBox();
Controls.add(textBox1);
}
Here, we try to execute the AddControl
function in a different thread, using the RunInThread
function. The RunInThread
function is executed in a separate thread. We try to call the AddControl
function inside the RunInThread
function through a delegate. Though the RunInThread
function runs in a separate thread, this.Invoke
(here, the 'this
' means the Form
object) puts the function to be executed in the send message queue. So, the function is ultimately executed in the main thread in which the form was created. So, it never throws an exception. (Note that, here the AddControl
and RunInThread
functions are not executed in the same thread.) The call to the Invoke
function will block the current thread until the AddControl
function is completed fully, i.e., the message box will appear only after the complete execution of the AddControl
call.
1.2.3 Control.BeginInvoke
As seen earlier, the BeginInvoke
method on the Control
class places the call into the post message queue. This call will ultimately be executed by the main thread, and the BeginInvoke
method will not block the current thread as Invoke
does. It will start executing the next line after placing the call in the post message queue. The return value can be collected using the EndInvoke
method.
Similar to the Invoke
method, BeginInvoke
has two overloads in the Control
class:
public System.IAsyncResult BeginInvoke ( System.Delegate method )
public virtual new System.IAsyncResult BeginInvoke ( System.Delegate method , object[] args )
The arguments are similar to those of BeginInvoke
, but the return type here is IAsynResult
. This IAsynresult
can be used for collecting the result using EndInvoke
.
Collapse Copy Code
Public delegate void MyDelegate();
Form_Load(……)
{
Thread t = new Thread(new ThreadStart(RunInThread));
t.Start();
}
void RunInThread()
{
MyDelagate delInstatnce = new MyDelagate(AddControl);
this.BeginInvoke(delInstatnce);
MessageBox.Show("Hello");
}
void AddControl()
{
TextBox textBox1 = new TextBox();
Controls.add(textBox1);
}
Since we have used the BeginInvoke
here, we could see the message box displayed before the AddControl
is completed.
Consider the following C# code:
Collapse Copy Code
public delegate void MyDelegate();
Form_Load(……)
{
MyDelagate delInstatnce = new MyDelagate(AddControl);
this.BeginInvoke(delInstatnce);
for(int i=0; i<1000; i++)
textBox1.Text += "Main " + i.ToString();
}
void AddControl()
{
for(int i=0; i<1000; i++)
textBox1.Text += "Thread " + i.ToString();
}
The above code will not run in parallel, since both the loops have to run in the main thread. First, it will print all the Form_Load
'for
' loop messages, and then it will print the AddControl
'for
' loop messages.
Consider the same C# code for the Invoke
call:
Collapse Copy Code
public delegate void MyDelegate();
Form_Load(……)
{
MyDelagate delInstatnce = new MyDelagate(AddControl);
this.Invoke(delInstatnce);
for(int i=0; i<1000; i++)
textBox1.Text += "Main " + i.ToString();
}
void AddControl()
{
for(int i=0; i<1000; i++)
textBox1.Text += "Thread " + i.ToString();
}
The above code will not run in parallel either, since both the loops have to run in the main thread. First, it will print all the AddControl
'for
' loop messages, and then it will print the Form_Load
'for
' loop messages. As opposed to the earlier one, the Invoke
method blocks the call.
1.2.4 Control.InvokeRequired
Consider the following C# code:
Collapse Copy Code
Form_Load(……)
{
MyDelagate delInstatnce = new MyDelagate(AddControl);
this.Invoke(delInstatnce);
}
void AddControl()
{
TextBox textBox1 = new TextBox();
Controls.add(textBox1);
}
In the above code, we unnecessarily use the Invoke
method, though we are there in the main thread already. Here, we know that we are in the main thread, and we can avoid using Invoke
/BeginInvoke
. But in some cases, we don't know whether the particular function will be executed by the main thread or by some other thread. And, the caller doesn't know whether the particular function has any UI update code. So, the function which updates the UI should take care of taking the current thread to the main thread. For that purpose, we have a member "InvokeRequired
" in the Control
class which returns true
if the current thread is not the main thread, false
if the current thread is the main thread. So, any update in the UI should happen through the InvokeRequired
property as a safe coding practice. The caller won't handle the Invoke
methods, rather the implementation inside the function will do.
The C# code follows:
Collapse Copy Code
Form_Load(……)
{
AddControl();
}
void RunInThread()
{
AddControl();
}
void AddControl()
{
if(this.InvokeRequired)
{
MyDelagate delInstatnce = new MyDelagate(AddControl);
this.Invoke(delInstatnce);
return;
}
TextBox textBox1 = new TextBox();
Controls.add(textBox1);
}
Here, the caller just calls the function, but the function implementation takes the current thread to the main thread, if required. In the above code, the Form_Load
call to AddControl
won't use the Invoke
method but the RunInThread
call to AddControl
will use the delegate Invoke
method.
The BeginInvoke
available in the delegate and the BeginInvoke
available in the Control
class are not the same. The BeginInvoke
on the delegates uses the thread pool threads to execute the function in parallel. But the BeginInvoke
in the Control
class just posts a message in the message queue. It will not create a separate thread to run the function. So, the delegate BeginInvoke
may enable parallelism, but the Control
BeginInvoke
may not.