KiMoGiGi 技术文集

不在乎选择什么,而在乎坚持多久……

IT博客 首页 联系 聚合 管理
  185 Posts :: 14 Stories :: 48 Comments :: 0 Trackbacks
摘至: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");
//Add your code that needs to be executed in separate thread 
//except UI updation
}
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(……) //this function runs in main thread.
{
AddControl();
}
void RunInThread() //this function runs in a different thread.
{
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.

posted on 2008-11-11 21:55 KiMoGiGi 阅读(634) 评论(0)  编辑 收藏 引用 所属分类: C# / Winforms
只有注册用户登录后才能发表评论。