|
An Overview
In the previous chapter, we saw how blocking I/O is performed on different streams, including network streams. However, blocking I/O is problematic because the I/O calls might do exactly what their name implies, block for an unknown amount of time. If an application does anything besides I/O operations, such as servicing a user interface, calling a blocking method will freeze the user interface and provide for a bad user experience. You can solve this problem with the Microsoft Windows .NET Framework concurrent programming model by using threads or by using the asynchronous programming pattern. Threads allow you to have one or more execution points in a program that can operate at the same time. When using threads, a thread can be spawned to perform any blocking I/O operations while your main application is freed to do other tasks, such as service the user interface. When a new thread is created, it begins executing from a method within your application. This method then executes all other threads in the application in parallel, including the main application thread. The asynchronous programming pattern, on the other hand, allows many of the classes in the .NET Framework to perform operations concurrently. With the asynchronous programming pattern, you can post operations such as a read or write to be asynchronously handled by the underlying class object, and the object will signal the application when the operation has completed. This way, the application can post a number of operations concurrently, which will not block, and your main application can perform other actions such as updating the user interface and responding to user events. This chapter introduces threads and the asynchronous pattern, which can enable you to design great networking applications. First we’ll describe the threading concept by showing how to create and manage threads to perform concurrent programming in an application where one or more threads can execute code while your main program continues to execute. Then we’ll describe thread synchronization concepts that are important to understand when using threads because of their concurrent programming behavior. Finally, after threads are described, we’ll talk about the asynchronous programming pattern that’s common to many classes in the .NET Framework and is similar in concept to the threading model.
Threading
Threading is a programming technique that permits two or more programming execution points (a place in your code) to run in an application at the same time. Threading is great because you can develop a single application that can do multiple things at the same time. For example, in Chapter 2, we described how to perform I/O over a network using synchronous programming techniques where your application could service only reading or writing data on a network and nothing else. But what if you want your application to handle several connections at the same time? Threading can help by allowing you to have multiple execution points in your code to service multiple connections in parallel. One thing to note is that you should be cautious about using too many threads to service connections, which we’ll discuss later in the chapter. Figure 3-1 demonstrates threading pictorially by describing a generic application having three threads that write data to a file, read data from a network, and update a screen. |
Figure 3-1: An application running three threads
The .NET Framework provides a Thread class in the System.Threading namespace that allows you to create and manage threads in your application.
Creating a thread is simple. All you have to do is create a thread object using the System.Threading.Thread class and pass a delegate method to the Thread constructor that will be called when the thread starts running. A delegate method can be any method that does not take a parameter and does not return a value. The following code fragment demonstrates one possible way to develop a delegate method:
C#
void MyThreadMethod()
{
// Do something useful here
}
Visual Basic .NET
Shared Sub MyThreadMethod()
' Do something useful here
End Sub
Once a delegate method is defined, you can create a thread. Creating a thread requires that you identify a delegate method to the Thread class constructor. A special class named ThreadStart allows you to define a delegate method to a thread. Once the delegate method is defined, you simply pass a ThreadStart object to the Thread constructor. The following code fragment shows how to define a ThreadStart delegate method using the MyThreadMethod() method mentioned above and create a thread:
C#
Thread MyThread = null;
try
{
ThreadStart ThreadMethod = new ThreadStart(MyThreadMethod);
MyThread = new Thread(ThreadMethod);
}
catch (Exception e)
{
Console.WriteLine("Failed to create thread with error: " + e.Message);
}
Visual Basic .NET
Dim MyThread As Thread = Nothing
Try
Dim ThreadMethod As ThreadStart = New ThreadStart(AddressOf MyThreadMethod)
MyThread = New Thread(ThreadMethod)
Catch e As Exception
Console.WriteLine("Failed to create thread with error: " + e.Message)
End Try
Once you’ve successfully created a thread, you can begin controlling how the thread operates within your program. When the thread is created, nothing actually happens from your application’s point of view; therefore, you’re required to call the thread’s Start() method to get the thread running. Calling Start() actually tells the operating system to begin scheduling your thread for processing. The following code fragment demonstrates how to start a thread using the MyThread object created earlier:
C#
try
{
MyThread.Start();
}
catch (Exception e)
{
Console.WriteLine("The thread failed to start with error: " + e.Message);
}
Visual Basic .NET
Try
MyThread.Start()
Catch e As Exception
Console.WriteLine("The thread failed to start with error: " + e.Message)
End Try
Once the thread is started, your delegate method will begin to run. A thread can be started only once. If you try to start the thread twice, a ThreadStateException will be raised, indicating that the thread can’t be started twice. Also, when the thread delegate method completes, you can’t restart the thread; if you try to, an exception will be raised.
Your application is allowed to run only a finite number of threads at the same time. If you try to start a thread while too many threads are running, Start() will throw an OutOfMemoryException. You might be wondering how many threads can start running in your application. It depends on operating system resources such as memory. In general, it’s a bad practice to run too many threads at the same time because scheduling threads for execution takes up operating system resources such as the computer processor and memory. It’s important to realize that a computer processor can actually service only one thread at a time, and when two threads are running, the operating system is switching control from one to the other. Operating system thread scheduling gives the application the illusion that each thread is running at the same time. If the application is running on a multiprocessor machine, the operating system can truly execute multiple threads at the same time. It’s important to understand that threading does not increase the amount of computing you can do in your application, but instead, it allows you to create more dynamic applications that can interact better with multiple resources at the same time instead of just doing one thing at a time.
Take note that the .NET Framework features code access security for many of the managed classes in the .NET Framework. However, threads can’t be controlled by code access security features in the .NET Framework version 1. A control flag for threads can be accessed by using the System.Security.Permissions.SecurityPermissionFlag.ControlThread permission from the System.Security.Permissions.SecurityPermission class. However, although the control flag exists, the security permission flag does not have any effect on controlling threads from a security zone.
Create a new CLR console application project and you might want to use ThreadExampleCP for the project and solution names as shown below.
Add the following code.
// ThreadExampleCP.cpp : main project file.
#include "stdafx.h"
using namespace System; using namespace System::Threading;
public ref class ThreadExample { public:
// The ThreadProc() method is called when the thread starts. // It loops ten times, writing to the console and yielding // the rest of its time slice each time, and then ends. static void ThreadProc() { for ( int i = 0; i < 10; i++ ) { Console::WriteLine("In ThreadProc(), another thread, pass #{0}",i); // Yield the rest of the time slice... Thread::Sleep(700); } } };
[STAThread] // Single threaded apartment int main(array<System::String ^> ^args) { Console::WriteLine("Main thread: Start a second thread..." ); // Create the thread, passing a ThreadStart delegate that // represents the ThreadExample::ThreadProc method. For a // delegate representing a static method, no object is required. Thread^ oldThread = gcnew Thread( gcnew ThreadStart( &ThreadExample::ThreadProc ) );
// Start ThreadProc(). Note that on a uniprocessor, the new // thread does not get any processor time until the main thread // is preempted or yields. Uncomment the Thread::Sleep that // follows oldThread->Start() to see the difference. oldThread->Start();
// Sleeping for main thread, so ThreadProc is running... // Thread::Sleep(1000); for ( int i = 0; i < 4; i++ ) { Console::WriteLine("Main thread: Do some work, pass #{0}", i); Thread::Sleep(1000); } Console::WriteLine("Main thread: Call Join(), to wait until ThreadProc() ends..." ); // Then main thread uses the Join method to wait for the new thread to terminate. // If not, a race condition would occur between the two threads. If this were to happen, // the second thread might not be able to finish processing before the application exits. oldThread->Join(); Console::WriteLine("Main thread: oldThread->Join() has returned.\nPress Enter to end program..." ); Console::ReadLine(); return 0; } |
Build and run the project. The following screenshot shows a sample output.
Create a new empty project. You can use the project and solution name given in the following Figure if you want.
Next, add a new class into the empty project. You can use the class name, ThreadExampleCS as given in the following Figure.
Add the following code.
using System; using System.Threading;
// <summary> // This is a simple sample that demonstrates how to create a thread that is // designed to update a shared variable named m_SomeNumber. The main program prints // the shared variable on 1 second intervals 10 times showing how the shared variable // has been changed by the thread routine. // </summary> namespace ThreadExampleCS { class ThreadExampleCS { // <summary> // The main entry point for the application. // </summary> static int m_SomeNumber = 0;
[STAThread] // Single threaded apartment static void Main(string[ ] args) { Thread MyThread = null;
try { Console.WriteLine("In Main()..."); ThreadStart ThreadMethod = new ThreadStart(MyThreadMethod); Console.WriteLine("ThreadStart() is OK...");
MyThread = new Thread(ThreadMethod); Console.WriteLine("Thread() is OK..."); } catch (Exception e) { Console.WriteLine("Failed to create thread with error: " + e.Message); return; }
try { MyThread.Start(); Console.WriteLine("Starting a thread using Start() is OK..."); } catch (Exception e) { Console.WriteLine("The thread failed to start with error: " + e.Message); }
for (int i = 0; i < 10; i++) { Console.WriteLine("Pass #" + i.ToString() + " The number is " + m_SomeNumber); Console.WriteLine("Thread is sleeping for a while using sleep(1000)..."); Thread.Sleep(1000); } }
public static void MyThreadMethod() { try { m_SomeNumber = 59;
Console.WriteLine(); Console.WriteLine("In MyThreadMethod()..."); for (int i = 0; i < 5; i++) { Console.WriteLine("Thread is sleeping for a while using sleep(5000)..."); Thread.Sleep(1000); m_SomeNumber += 5; } } catch (ThreadAbortException e) { Console.WriteLine("Caught thread abort exception: " + e.Message); } } } } |
Build and run the project.
The following snapshot shows the output sample.
![]() |
|
While the program is running, you may want to see the process and the threads count through the Windows Task Manager (Ctrl + Alt + Del).
Create a new class library project. You can use the project and solution name as given in the following Figures if you want.
Add the following code.
Imports System Imports System.Threading
' ThreadSample is a simple sample that demonstrates how to create a thread that is ' designed to update a shared variable named m_SomeNumber. The main program prints ' the shared variable on 1 second intervals 10 times showing how the shared variable ' has been changed by the thread routine.
Public Class Class1 'The main entry point for the application. Shared m_SomeNumber As Integer = 0
Shared Sub MyThreadMethod() Try m_SomeNumber = 59 Console.WriteLine("In MyThreadMethod() sub...") Dim i As Integer For i = 0 To 4 Console.WriteLine("Sleep(1000)...") Thread.Sleep(1000) m_SomeNumber += 5 Next Catch e As ThreadAbortException Console.WriteLine("Caught thread abort exception: " + e.Message) End Try End Sub
Shared Sub Main()
Dim MyThread As Thread = Nothing
Try Console.WriteLine("In Main() sub...") Dim ThreadMethod As ThreadStart = New ThreadStart(AddressOf MyThreadMethod) Console.WriteLine("ThreadStart() is OK...") MyThread = New Thread(ThreadMethod) Console.WriteLine("Thread() is OK...") Catch e As Exception Console.WriteLine("Failed to create thread with error: " + e.Message) End Try
Try MyThread.Start() Console.WriteLine("Start() is OK...") Catch e As Exception Console.WriteLine("The thread failed to start with error: " + e.Message) End Try
Dim i As Integer For i = 0 To 9 Console.WriteLine("Pass #" + i.ToString() + " The number is " + m_SomeNumber.ToString()) Console.WriteLine("Sleep(1000)...") Thread.Sleep(1000) Next End Sub End Class |
Change the Application type: to Console Application and Startup object: to Sub Main through the project Properties.
Build and run the project. The following is a sample output.