|
Controlling a Thread
A thread has several operating states that identify its current operating status. Table 3-1 describes the available states. Several Thread class methods - Abort(), Suspend(), Interrupt(), and Resume(), control thread execution inside your program. Each of these methods can change the operating state of your thread.
Abort()
The Abort() method is designed to stop a thread from running in a controllable, well-defined manner. Abort() causes a special ThreadAbortException to be immediately raised in the thread delegate method and is the best way to stop a thread dead in its tracks. If your thread delegate does not catch the exception, the thread is automatically stopped and the thread state changes to Stopped. If the delegate catches the exception, the delegate has an opportunity to cancel the abort request by calling the static ResetAbort() method of the Thread class within the delegate catch block. When ResetAbort() is called, the delegate might continue processing outside the originating try/catch block that caught the exception. The following code fragment shows how to catch the ThreadAbortException in a delegate method. We also placed comments that describe how you can cancel the abort operation using the ResetAbort() method. |
C#
public static void MyThreadMethod()
{
try
{
// Do something useful here
}
catch (ThreadAbortException e)
{
Console.WriteLine("Caught thread abort exception: " + e.Message);
// We could call Thread.ResetAbort here, and the delegate will
// continue processing after this try - catch block.
}
}
Visual Basic .NET
Shared Sub MyThreadMethod()
Try
' Do something useful here
Catch e As ThreadAbortException
Console.WriteLine("Caught thread abort exception: " + e.Message)
' We could call Thread.ResetAbort here, and the delegate will
' continue processing after this try - catch block.
End Try
End Sub
Another possible way you can stop a thread from running is by calling Suspend() or Interrupt(). Suspend() is different from Abort() because with Suspend(), your thread delegate method only pauses and does not experience an exception. Calling Suspend() will immediately suspend a thread from running and allows you to continue running the thread at a later time by calling Resume().
Calling Interrupt() on a thread will also suspend a thread from running, but only if the delegate routine calls the Monitor.Wait(), Thread.Sleep(), or Thread.Join() method during processing. If the delegate routine never calls one of these methods, the thread will not be interrupted. If your thread does get interrupted and you need to get the thread running again, you’ll have to call the Resume() method.
When your thread starts running after calling Start(), it runs by default in the foreground, meaning that your main program will wait for your thread to complete before the main program can exit. This wait is great because your main program will not have to make special code to wait on one or more threads to complete, especially if your threads are doing something critical in your application such as saving important data to a file. If you don’t want the main program to wait for your thread to complete, you can change the Thread class property IsBackground from the default Boolean value false to the value true. When the value is true, your thread will run in the background. If the main program exits while your thread is running in the background, the common language runtime will automatically invoke the Abort() method on your thread when the main program exits. If your thread is not doing something critical to your application, running a thread in the background is advisable.
If you need to know when a thread has completed or if you want to wait on a thread to complete, you can call Join(). Join() is designed to wait on a thread until the thread completes, or it can wait for a specified amount of time until the time expires or the thread completes.
As we have discussed so far in this chapter, we highly recommend using threads to perform blocking operations asynchronously, which enables you to develop applications that provide a much better experience for the user. This recommendation is especially true when working with Windows Forms. Rather than have the form freeze up while you’re waiting on a blocking call, everything in the user’s screen remains nice and responsive.
When working with threads while developing a Windows Forms application, there’s a good chance that you might want to perform I/O on a Windows Form control such as a text box while running a thread delegate method. Doing so presents an I/O problem because the form is designed to process I/O within a Windows Form thread while your delegate method runs in another thread. For a Windows Form to handle I/O correctly, you must allow the Windows Form thread to coordinate (or marshal) the I/O calls. The form processes these calls sequentially, so you should not write directly to the form from another thread.
To allow a Windows Form to coordinate I/O, a special method is available in the forms (System.Windows.Forms.Form) object named Control.Invoke() that’s designed to marshal the I/O calls for a forms thread. Control.Invoke() accepts a delegate method as a parameter and will run your delegate method from within the Windows Forms thread to coordinate I/O. The following programming steps can help you successfully use Invoke() to coordinate I/O from a thread. This code assumes that the delegate methods are defined within your form class, so we can easily reference form objects using the this object.
1. Define a method that will write a string message to your form. The following method is designed to send a message to a text box in a Windows Form:
Public Sub MyTextBoxWriter (ByVal MessageToWrite As String)
Me.StatusBar.Text = MessageToWrite
End Sub
2. Define a delegate method description for the method in step 1.
Public Delegate Sub myDelegateMethod(ByVal Message As String)
3. Declare an instance of your form writer method as a delegate.
Dim SB As myDelegateMethod = New myDelegateMethod(AddressOf StatusBarWriter)
4. Call your forms Control.Invoke() method from any thread in your application to marshal the delegate to the forms thread to perform I/O. Because the delegate method is designed to take a string as a parameter, we have to set up an argument array with a string as the first element and pass it to Invoke().
Private Sub WriteToStatusBar(ByVal Message As String)
Dim SB As StatusBarWriterDelegateMethod = New StatusBarWriterDelegateMethod(AddressOf StatusBarWriter)
SB.Invoke(Message)
End Sub
Additionally, there is an asynchronous version of Invoke() named BeginInvoke() and a results method named EndInvoke() that will allow you to perform Invoke using the .NET Framework asynchronous pattern. The .NET Framework asynchronous pattern design is discussed in the “Asynchronous Pattern” section later in this chapter.
Create a new Windows Forms application project and you might want to use WinNetworkIOCP for the project and solution names as shown below.
Drag, drop and rearrange the related controls from the Toolbox window as shown in the following Windows form.
Customize the form using the following information.
Control |
Property |
Value |
Form1 |
Text |
WinNetworkIOCP |
Button |
Text |
Connect, Send, Close |
|
Name |
ConnectButton |
Label1 |
Text |
IP Address: |
TextBox1 |
Name |
IPAddressBox |
|
Text |
127.0.0.1 |
Label2 |
Text |
Port: |
TextBox2 |
Name |
PortBox |
|
Text |
5150 |
Label3 |
Text |
Message to send: |
TextBox3 |
Name |
SendMessageBox |
|
Text |
This is a test message… |
|
Multiline |
True |
The following Figure shows the almost complete Windows form.
Next, we would like to add status bar that can display messages for the actions that happens. Drag and drop a StatusStrip container component at the bottom of the Windows form.
Then, add a StatusLabel for the StatusStrip. Click the StatusStrip1’s down arrow and select StatusLabel as shown below.
Do some customization for the StatusLabel. Select the statusStrip1 > right click mouse > select Edit Items context menu.
Select the StatusLabel1 (under the statusStrip1) in the left window (Members:). In the property page on the right, change the Text property’s value to None and Name property’s value to StatusBar. Click OK.
The following is a completed Windows form design for this project. Now, we are ready to add codes. You can build and run this project at this stage.
Adding codes for the actions. First of all, double click the top button. This will bring the code page for a button click event. Add the following two lines code to the ConnectButton_Click() method.
private: System::Void ConnectButton_Click(System::Object^ sender, System::EventArgs^ e) { DisableFields(); DoNetworkingConnection(); } |
Don’t forget to add the following using directives.
using namespace System::Net; using namespace System::Net::Sockets; using namespace System::Threading; using namespace System::IO; |
![]() |
|
Continue adding more codes (DisabledFields() and EnabledFields() methods) just after the ConnectButton_Click() closing bracket. These methods will disable and enable the fields in the form respectively when invoked.
private: void DisableFields() { PortBox->Enabled = false; IPAddressBox->Enabled = false; SendMessageBox->Enabled = false; ConnectButton->Enabled = false; }
private: void EnableFields() { PortBox->Enabled = true; IPAddressBox->Enabled = true; SendMessageBox->Enabled = true; ConnectButton->Enabled = true; } |
Add the following code to write messages to the status bar of the Windows form.
private: void WriteToStatusBar(String^ Message) { EnableFields(); StatusBar->Text = Message; } |
Then, add code for the threading, DoNetworkingConnection() method.
private: void DoNetworkingConnection() { Thread^ MyThread = nullptr;
try { ThreadStart^ ThreadMethod = gcnew ThreadStart(this, &Form1::ConnectTo); MyThread = gcnew Thread(ThreadMethod); } catch (Exception^ e) { WriteToStatusBar("Failed to create thread with error: " + e->Message); // return 0; }
try { MyThread->Start(); } catch (Exception^ e) { WriteToStatusBar("The thread failed to start with error: " + e->Message); } } |
Finally, add the following code for the real network connection setup.
private: void ConnectTo() { String^ ServerName = this->IPAddressBox->Text; int Port = Convert::ToInt32(this->PortBox->Text); WriteToStatusBar("IP Address: " + ServerName + "Port: " + Port); Socket^ ClientSocket = nullptr;
try { // Let's connect to a listening server try { ClientSocket = gcnew Socket(AddressFamily::InterNetwork, SocketType::Stream, ProtocolType::IP); WriteToStatusBar("Socket is OK..."); } catch (Exception^ e) { WriteToStatusBar("Failed to create client Socket: " + e->Message); }
IPEndPoint^ ServerEndPoint = gcnew IPEndPoint(IPAddress::Parse(ServerName), Convert::ToInt16(Port));
try { ClientSocket->Connect(ServerEndPoint); WriteToStatusBar("Connect() is OK..."); } catch (Exception^ e) { ClientSocket->Close(); WriteToStatusBar("Failed to connect client Socket: " + e->Message); } } catch (Exception^ e) { WriteToStatusBar(e->Message); ClientSocket->Close(); }
// Let's create a network stream to communicate over the connected Socket.
// This not supposed to be set to nullptr, because if the catch/Finally block // invoked, the famous "An unhandled exception of type 'System.NullReferenceException' // occurred in executable_file_name" & "Object reference not set to an instance of an object" // Exception messages… // You may want to change this crap code to avoid 'System.NullReferenceException' // by initializing the ClientNetworkStream to something valid instead of nullptr... NetworkStream^ ClientNetworkStream = nullptr;
try { try { // Setup a network stream on the client Socket ClientNetworkStream = gcnew NetworkStream(ClientSocket, true); WriteToStatusBar("Instantiating NetworkStream..."); } catch (Exception^ e) { // We have to close the client socket here because the network stream did not take ownership of the socket. ClientSocket->Close(); WriteToStatusBar("Failed to create a NetworkStream with error: " + e->Message); }
StreamWriter^ ClientNetworkStreamWriter = nullptr;
try { // Setup a Stream Writer ClientNetworkStreamWriter = gcnew StreamWriter(ClientNetworkStream); WriteToStatusBar("Setting up StreamWriter..."); } catch (Exception^ e) { ClientNetworkStream->Close(); WriteToStatusBar("Failed to create a StreamWriter with error: " + e->Message); }
try { ClientNetworkStreamWriter->Write(this->SendMessageBox->Text->ToString()); ClientNetworkStreamWriter->Flush(); WriteToStatusBar("We wrote " + this->SendMessageBox->Text->Length.ToString() + " character(s) to the server."); } catch (Exception^ e) { WriteToStatusBar("Failed to write to client NetworkStream with error: " + e->Message); } } catch (Exception^ e) { WriteToStatusBar("Error lor!!!: " + e->Message); // Just exit if there is no receiver... // Environment::Exit(0); } finally { // Close the network stream once everything is done ClientNetworkStream->Close(); } } |
Before building and running this project, change the [STAThreadAttribute] to [STAThread]. Double click the WinNetworkIOCP.cpp and put the [STAThread] attribute.
Build the project and run it in Debug mode. The following is the sample output. Fill-in a message in the Message to send: text box and click the Connect, Send, Close button.
The following shows the result of the previous action. Notice the message displayed in the status bar. In this case we don’t have any receiver to receive the message, so the catch block has been reached and the program try to close the non existent client network stream (we initialize the stream to nullptr at the beginning.)
Let test this program with a receiver. In this case we will use the previously created receiver program, networkstreamsamplecpreceiver.exe. Firstly, run the receiver program with port number 1000. The following is the output screen for the receiver program that waiting a connection from sender.
Then, run the Windows form program using a localhost and port number 1000. You may want to change the message to send to.
The following is the receiver screenshot output sample when the communication was completed successfully.
A complete added code for this part is shown below.
private: System::Void ConnectButton_Click(System::Object^ sender, System::EventArgs^ e) { DisableFields(); DoNetworkingConnection(); }
private: void DisableFields() { PortBox->Enabled = false; IPAddressBox->Enabled = false; SendMessageBox->Enabled = false; ConnectButton->Enabled = false; }
private: void EnableFields() { PortBox->Enabled = true; IPAddressBox->Enabled = true; SendMessageBox->Enabled = true; ConnectButton->Enabled = true; }
private: void WriteToStatusBar(String^ Message) { EnableFields(); StatusBar->Text = Message; }
private: void DoNetworkingConnection() { Thread^ MyThread = nullptr;
try { ThreadStart^ ThreadMethod = gcnew ThreadStart(this, &Form1::ConnectTo); MyThread = gcnew Thread(ThreadMethod); } catch (Exception^ e) { WriteToStatusBar("Failed to create thread with error: " + e->Message); // return 0; }
try { MyThread->Start(); } catch (Exception^ e) { WriteToStatusBar("The thread failed to start with error: " + e->Message); } }
private: void ConnectTo() { String^ ServerName = this->IPAddressBox->Text; int Port = Convert::ToInt32(this->PortBox->Text);
WriteToStatusBar("IP Address: " + ServerName + "Port: " + Port); Socket^ ClientSocket = nullptr;
try { // Let's connect to a listening server try { ClientSocket = gcnew Socket(AddressFamily::InterNetwork, SocketType::Stream, ProtocolType::IP); WriteToStatusBar("Socket is OK..."); } catch (Exception^ e) { WriteToStatusBar("Failed to create client Socket: " + e->Message); }
IPEndPoint^ ServerEndPoint = gcnew IPEndPoint(IPAddress::Parse(ServerName), Convert::ToInt16(Port));
try { ClientSocket->Connect(ServerEndPoint); WriteToStatusBar("Connect() is OK..."); } catch (Exception^ e) { ClientSocket->Close(); WriteToStatusBar("Failed to connect client Socket: " + e->Message); } } catch (Exception^ e) { WriteToStatusBar(e->Message); ClientSocket->Close(); }
// Let's create a network stream to communicate over the connected Socket.
// This not supposed to be set to nullptr, because if the catch/Finally block // invoked, the famous "An unhandled exception of type 'System.NullReferenceException' // occurred in executable_file_name" & "Object reference not set to an instance of an object" // Exception messages… // You may want to change this crap code to avoid 'System.NullReferenceException' // by initializing the ClientNetworkStream to something valid instead of nullptr... NetworkStream^ ClientNetworkStream = nullptr;
try { try { // Setup a network stream on the client Socket ClientNetworkStream = gcnew NetworkStream(ClientSocket, true); WriteToStatusBar("Instantiating NetworkStream..."); } catch (Exception^ e) { // We have to close the client socket here because the network // stream did not take ownership of the socket. ClientSocket->Close(); WriteToStatusBar("Failed to create a NetworkStream with error: " + e->Message); }
StreamWriter^ ClientNetworkStreamWriter = nullptr;
try { // Setup a Stream Writer ClientNetworkStreamWriter = gcnew StreamWriter(ClientNetworkStream); WriteToStatusBar("Setting up StreamWriter..."); } catch (Exception^ e) { ClientNetworkStream->Close(); WriteToStatusBar("Failed to create a StreamWriter with error: " + e->Message); }
try { ClientNetworkStreamWriter->Write(this->SendMessageBox->Text->ToString()); ClientNetworkStreamWriter->Flush(); WriteToStatusBar("We wrote " + this->SendMessageBox->Text->Length.ToString() + " character(s) to the server."); } catch (Exception^ e) { WriteToStatusBar("Failed to write to client NetworkStream with error: " + e->Message); } } catch (Exception^ e) { WriteToStatusBar("Error lor!!!: " + e->Message); // Just exit if there is no receiver... // Environment::Exit(0); } finally { // Close the network stream once everything is done ClientNetworkStream->Close(); } } |