Home > Articles

  • Print
  • + Share This
  • 💬 Discuss
This chapter is from the book

Creating Multithreaded Applications

For Scrabble players, "multitasking" and "multithreading" might be a great opportunity to earn points. For developers, these terms are often sources of confusion and unnecessary headaches. I should emphasize unnecessary here because, when explained, they become part of the obvious programming concepts.

Understanding Multitasking

To put it simply, multitasking is the capability of the operating system to run multiple programs at the same time. Unconsciously, you've been using this capability while switching from your Microsoft Word document to Windows Explorer. Although multitasking might seem characteristic of graphical operating systems such as Windows or Linux, earlier computers also used multitasking to some extent. For example, Unix enables you to run multiple programs in the background.

Under Windows 3.x, applications used cooperative multitasking. Cooperative means that a program has control over the CPU and, before switching to another application, this program must finish processing data. This type of multitasking has a serious drawback: If an application stops responding, the entire operating system will hang. 32-bit versions of Windows solved this problem by introducing preemptive multitasking. A simple dictionary definition will help you understand its meaning:

preemptive: done before somebody else has had an opportunity to act.

In other words, to allow task-switching, 32-bit versions of Windows suspend the current application, whether it is ready to lose control or not.

NOTE

Cooperative multitasking is also called "nonpreemptive multitasking" for obvious reasons. Unlike preemptive multitasking, the operating system is unable to suspend an application that has stopped responding.

Understanding Multithreading

Multithreading is the capability of a program to run multiple tasks (threads) at the same time. Most Windows applications use only one thread, the primary thread. A primary thread takes care of child windows creation and message processing. All secondary threads are used to perform background operations: loading large files, looking for information, performing mathematical calculations.

WARNING

Throughout their learning process, young children tend to repeat words they have overheard here and there, simply to prove their knowledge or to resemble "big people." A similar situation occurs with programmers. Some developers tend to overuse programming techniques they've learned.

Do not use separate threads in your application unless you're dealing with lengthy background operations. Sometimes, with small code readjustments, you can simply avoid the use of threads. Why complicate your work? That said, multithreaded applications offer advantages, as you'll see later in this chapter.

Creating a Thread Using API Calls

You can create a new thread from another one by calling the CreateThread() API function. The CreateThread() parameters specify, among other things, the security attributes, the creation flags, and the thread function:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  DWORD dwStackSize,
  LPTHREAD_START_ROUTINE lpStartAddress,
  LPVOID lpParameter,
  DWORD dwCreationFlags,
  LPDWORD lpThreadId
  );

The SECURITY_ATTRIBUTES structure determines whether other processes can modify the object and whether a child process can inherit a handle to this object. If lpThreadAttributes is NULL, the thread gets the default security descriptor.

The dwCreationFlags parameter specifies the thread creation flags. If its value is CREATE_SUSPENDED, the thread will not run until you call the ResumeThread() function. Set this value to 0 to run the thread immediately after creation.

The lpThreadId parameter points to an empty DWORD that will receive the thread identifier. Under Windows NT/2000, if this parameter is NULL, the thread identifier is simply not returned. Windows 9x requires a DWORD variable. To ensure complete compatibility with the current operating system, do not use the NULL value.

The most crucial parameter is the starting function, also known as thread function. lpStartAddress is the address of the function that accepts one parameter and returns a DWORD exit code:

DWORD WINAPI ThreadFunc(LPVOID);

TIP

In a sense, the starting function can be compared to the main() or WinMain() function in a C++ program. ThreadFunc() is the main entry point for your thread.

Finally, dwStackSize and lpParameter specify the size of the stack (in bytes) and the parameter passed to the thread, respectively.

TIP

CreateThread(), as many other API calls, contains a large list of arguments more or less complex. In the beginning, understanding all aspects of this function can be disorienting.

A simple trick to overcome this problem is to first look at the arguments that can be zeroed. For example, in almost all parameters of CreateThread() except for lpStartAddress and lpThreadId, you can safely specify 0. After you fully understand these two arguments, you can always go back and further explore the CreateThread() function.

With the previous explanations and a little help from the Win32 Programmer's Reference help file, you should now be able to write a simple multithreaded application. Your example project should contain two buttons: Start and Stop. When the user clicks the Start button, this resumes the newly created thread. The thread should draw random ellipses and rectangles on the form. By clicking the Stop button, the user should be able to suspend the thread. Take a look at Listing 3.14. Don't forget that you can find the complete source code in the ThreadAPI folder of the companion CD-ROM.

Listing 3.14 ThreadFormUnit.cpp

#include <vcl.h>
#pragma hdrstop

#include "ThreadFormUnit.h"
#pragma package(smart_init)
#pragma resource "*.dfm"

TThreadForm *ThreadForm;
HANDLE Thread;

DWORD WINAPI ThreadFunc(LPVOID Param)
{
  HANDLE MainWnd(Param);

  RECT R;
  GetClientRect(MainWnd, &R);

  const MaxWidth = R.right - R.left;
  const MaxHeight = R.bottom - R.top;
  int X1, Y1, X2, Y2, R1, G1, B1;
  bool IsEllipse;

  while(true)
  {
   HDC DC = GetDC(MainWnd);

   X1 = rand() % MaxWidth;
   Y1 = rand() % MaxHeight;
   X2 = rand() % MaxWidth;
   Y2 = rand() % MaxHeight;

   R1 = rand() & 255;
   G1 = rand() & 255;
   B1 = rand() & 255;

   IsEllipse = rand() & 1;

   HBRUSH Brush = CreateSolidBrush(
     RGB(R1, G1, B1));
   SelectObject(DC, Brush);

   if(IsEllipse)
     Ellipse(DC, X1, Y1, X2, Y2);
   else
     Rectangle(DC, X1, Y1, X2, Y2);

   ReleaseDC(MainWnd, DC);
   DeleteObject(Brush);
  }
}

__fastcall TThreadForm::TThreadForm(TComponent* Owner)
  : TForm(Owner)
{
  randomize();

  DWORD Id;
  Thread = CreateThread(0, 0, ThreadFunc,
   ThreadForm->Handle, CREATE_SUSPENDED, &Id);

  if(!Thread)
  {
   ShowMessage("Error! Cannot create thread.");
   Application->Terminate();
  }
}

void __fastcall TThreadForm::StartClick(TObject *)
{
  ResumeThread(Thread);
  Start->Enabled = false;
  Stop->Enabled = true;
}

void __fastcall TThreadForm::StopClick(TObject *)
{
  SuspendThread(Thread);
  Stop->Enabled = false;
  Start->Enabled = true;
}

NOTE

As you can see in Listing 3.14, the code uses API functions almost exclusively. One of the reasons is that you must avoid accessing VCL properties and methods from secondary threads. I will describe why and provide a solution in the next section.

In the form constructor, you create a suspended thread using the CreateThread() function and check whether the new thread is valid or not. The Start and Stop buttons use the ResumeThread() and SuspendThread() API functions to modify the thread state. Finally, the thread function draws random shapes on the form's canvas. (Notice how the window handle is passed to ThreadFunc().) Figure 3.16 shows the project.

Figure 3.16Figure 3.16 The ThreadAPI project.

NOTE

Another efficient way to start a new thread is the _beginthread() routine defined in process.h (you can find process.h in the C++Builder installation Include folder):

unsigned long _beginthread(void (_USERENTRY *__start)(void *),
 unsigned __stksize, void *__arg);

NOTE

Because it requires fewer parameters, this function is commonly used in multithreaded applications.

Understanding the TThread Object

C++Builder encapsulates Windows thread objects into the TThread object. Creating a new thread is basically a matter of creating a new instance of a TThread descendant. Listing 3.15 contains the definition of the TThread abstract class.

Listing 3.15 TThread Class

class DELPHICLASS TThread;
class PASCALIMPLEMENTATION TThread : public System::TObject
{
  typedef System::TObject inherited;

private:
  unsigned FHandle;
  unsigned FThreadID;
  bool FTerminated;
  bool FSuspended;
  bool FFreeOnTerminate;
  bool FFinished;
  int FReturnValue;
  TNotifyEvent FOnTerminate;
  TThreadMethod FMethod;
  System::TObject* FSynchronizeException;
  void __fastcall CallOnTerminate(void);
  TThreadPriority __fastcall GetPriority(void);
  void __fastcall SetPriority(TThreadPriority Value);
  void __fastcall SetSuspended(bool Value);

protected:
  virtual void __fastcall DoTerminate(void);
  virtual void __fastcall Execute(void) = 0 ;
  void __fastcall Synchronize(TThreadMethod Method);
  __property int ReturnValue = {read=FReturnValue, write=FReturnValue,
     nodefault};
  __property bool Terminated = {read=FTerminated, nodefault};

public:
  __fastcall TThread(bool CreateSuspended);
  __fastcall virtual ~TThread(void);
  void __fastcall Resume(void);
  void __fastcall Suspend(void);
  void __fastcall Terminate(void);
  unsigned __fastcall WaitFor(void);
  __property bool FreeOnTerminate = {read=FFreeOnTerminate, write=
     FFreeOnTerminate, nodefault};
  __property unsigned Handle = {read=FHandle, nodefault};
  __property TThreadPriority Priority = {read=GetPriority, write=
     SetPriority, nodefault};
  __property bool Suspended = {read=FSuspended, write=SetSuspended,
     nodefault};
  __property unsigned ThreadID = {read=FThreadID, nodefault};
  __property TNotifyEvent OnTerminate = {read=FOnTerminate, write=
     FOnTerminate};
};

If you're wondering how to create a TThread descendant, the answer is simple. Open the File, New dialog and select Thread Object from the Object Repository. C++Builder will prompt you for the class name of the new descendant. Enter TRandomThread and click OK.

C++Builder will create automatically a new source file containing the TRandomThread object:

#include <vcl.h>
#pragma hdrstop

#include "Unit2.h"
#pragma package(smart_init)

__fastcall TRandomThread::TRandomThread(bool CreateSuspended)
  : TThread(CreateSuspended)
{
}

void __fastcall TRandomThread::Execute()
{

}

The Execute() method contains the code that will be executed when the thread runs. In other words, Execute() replaces your thread function. Also notice that the constructor of your object contains a CreateSuspended parameter. Just like the CREATE_SUSPENDED flag, when CreateSuspended is true, you must first call the Resume() method; otherwise, Execute() won't be called.

Tables 3.3 and 3.4 summarize the most common properties and methods of the TThread class.

Table 3.3 TThread Properties

Property

Description

FreeOnTerminate

Determines whether the thread object is automatically destroyed when the thread terminates.

Handle

Provides access to the thread's handle. Use this value when calling API functions.

Priority

Specifies the thread's scheduling priority. Set this priority to a higher or lower value when needed.

ReturnValue

Determines the value returned to other threads when the current thread object finishes.

Suspended

Specifies whether the thread is suspended or not.

Terminated

Determines whether the thread is about to be terminated.

ThreadID

Determines the thread's identifier.


Table 3.4 TThread Methods

Method

Description

DoTerminate()

Calls the OnTerminate event handler without terminating the thread.

Execute()

Contains the code to be executed when the thread runs.

Resume()

Resumes a suspended thread.

Suspend()

Pauses a running thread.

Synchronize()

Executes a call within the VCL primary thread.

Terminate()

Signals the thread to terminate.

WaitFor()

Waits for a thread to terminate.


Now it's time to try to use VCL objects exclusively.

You've already created a TRandomThread object, so use this object as the secondary thread of your application. The first step is to add the main unit's include file to the new thread unit. Select File, Include Unit Hdr and then select ThreadFormUnit.

There's not much to put in the TRandomThread constructor, except for the random numbers generator:

__fastcall TRandomThread::TRandomThread(bool
  CreateSuspended) : TThread(CreateSuspended)
{
  randomize();
}

Now take care of the core part of your thread: the Execute() method. You no longer need to determine the form size using the GetClientRect() API function. You can simply read the ClientWidth and ClientHeight properties:

  const MaxWidth = ThreadForm->ClientWidth;
  const MaxHeight = ThreadForm->ClientHeight;
  int X1, Y1, X2, Y2, R1, G1, B1;
  bool IsEllipse;

The TCanvas object, with which you're probably familiar, can greatly simplify the drawing process. There is a small problem: The VCL does not allow multiple threads to access the same graphic object simultaneously. Therefore, you must use the Lock() and Unlock() methods to make sure that other threads do not access the TCanvas while you're drawing:

  while(true)
  {
   ThreadForm->Canvas->Lock();

   X1 = rand() % MaxWidth;
   Y1 = rand() % MaxHeight;
   X2 = rand() % MaxWidth;
   Y2 = rand() % MaxHeight;

   R1 = rand() & 255;
   G1 = rand() & 255;
   B1 = rand() & 255;

   IsEllipse = rand() & 1;

   ThreadForm->Canvas->Brush->Color =
     TColor(RGB(R1, G1, B1));

   if(IsEllipse)
     ThreadForm->Canvas->Ellipse(X1, Y1,
      X2, Y2);
   else
     ThreadForm->Canvas->Rectangle(X1, Y1,
      X2, Y2);

   ThreadForm->Canvas->Unlock();
  }

This puts an end to the thread object code. Take a look now at the main unit. In the form constructor, you create a new instance of TRandomThread:

TRandomThread* Thread;

__fastcall TThreadForm::TThreadForm(TComponent* )
  : TForm(Owner)
{
  Thread = new TRandomThread(true);
  if(!Thread)
  {
   ShowMessage("Error! Cannot create thread.");
   Application->Terminate();
  }
}

The Start button calls the Resume() method:

void __fastcall TThreadForm::StartClick(TObject *)
{
  Thread->Resume();
  Start->Enabled = false;
  Stop->Enabled = true;
}

The Stop button calls the Suspend() method:

void __fastcall TThreadForm::StopClick(TObject *)
{
  Thread->Suspend();
  Stop->Enabled = false;
  Start->Enabled = true;
}

TIP

The C++Builder IDE provides a Threads debug window containing the list of available threads: their ID, state, location, and status. To display this window, choose View, Debug Windows, Threads from the C++Builder menu or press Ctrl+Alt+T.

The thread is automatically terminated when the Execute() function finishes executing or when the application is closed. To ensure that memory occupied by your thread object is freed on termination, always insert the following in the Execute() method:

  FreeOnTerminate = true;

Sometimes, however, you might need to terminate a thread by code. To do so, you could use the Terminate() method. Terminate() tells the thread to terminate by setting the Terminated property to true.

It is important to understand that Terminate() does not exit the thread by itself. You must periodically check in the Execute() method whether Terminated is true. For example, to terminate your TRandomThread object, add the following line:

  while(true)
  {
   if(Terminated) break;

Terminate() has the advantage of enabling you to do the cleaning by yourself, thus giving you more control over the thread termination. Unfortunately, if the thread stops responding, calling Terminate() will be useless.

The TerminateThread() API function is a more radical way to cause a thread to exit. TerminateThread() instantly closes the current thread without freeing memory occupied by the thread object. You should use this function only in extreme cases, when no other options are left. The TerminateThread() syntax is simple. Here is an example:

TerminateThread((HANDLE)Thread->Handle, false);

Understanding the Main VCL Thread

Properties and methods of VCL objects are not necessarily thread safe. This means that when accessing properties and methods, you can use memory that is not protected from other threads. Therefore, the main VCL thread should be the only thread to have control over the VCL.

NOTE

The main VCL thread is the primary thread of your application. It handles and processes Windows messages received by VCL controls.

NOTE

Graphic objects are exceptions to the thread-safe rule. By using the Lock() and Unlock() methods, other threads can be prevented from drawing on the canvas.

To allow threads to access VCL objects, TThread provides the Synchronize() method. Synchronize() performs actions contained in a routine as if they were executed from the main VCL thread:

void __fastcall Synchronize(TThreadMethod &Method);

Consider the example of a thread displaying increasing values in a Label component. Obviously, you'll use a for loop in the Execute() method. But, how will you change the Label's caption? By synchronizing with the VCL. Listing 3.16 contains the source code of the TLabelThread object, and Figure 3.17 shows the results.

Listing 3.16 TLabelThread Thread Object

#include <vcl.h>
#pragma hdrstop

#include "ThreadFormUnit.h"
#pragma package(smart_init)

#include <Classes.hpp>

class TLabelThread : public TThread
{
private:
protected:
  int Num;
  void __fastcall Execute();
  void __fastcall DisplayLabel();
public:
  __fastcall TLabelThread(bool CreateSuspended);
};
//-----------------------------------------------
__fastcall TLabelThread::TLabelThread(bool
  CreateSuspended) : TThread(CreateSuspended)
{
}

void __fastcall TLabelThread::DisplayLabel()
{
  ThreadForm->Label->Caption = Num;
}

void __fastcall TLabelThread::Execute()
{
  FreeOnTerminate = true;
  for(Num = 0; Num <= 1000; Num++)
  {
   if(Terminated) break;
   Synchronize (DisplayLabel);
  }
}

TIP

As opposed to the TRandomThread example, where you had an endless loop, in this project the thread is terminated when the value of 1000 is reached. By handling the OnTerminate event of TLabelThread, you can determine when the thread is about to exit:

void __fastcall TThreadForm::bStartClick(TObject *)

{
 Thread = new TLabelThread(false);
 Thread->OnTerminate = OnTerminate;
 bStart->Enabled = false;
}

void __fastcall TThreadForm::OnTerminate(TObject *)
{
 bStart->Enabled = true;
}

In fact, you can use OnTerminate as a replacement for the Synchronize() method. If your thread has actions to perform before exiting, OnTerminate will enable you to access VCL properties and methods from within the main unit.

Consider the previous example where you enabled the bStart button in the OnTerminate event handler. To accomplish the same thing directly from the thread object, you would have written a far more complex code:

// void __fastcall EnableButton();

void __fastcall TLabelThread::EnableButton()
{
 ThreadForm->bStart->Enabled = true;
}

void __fastcall TLabelThread::Execute()
{
 // ...
 if(Terminated)
 {
   Synchronize(EnableButton);
 // ...
}
Figure 3.17Figure 3.17 The LabelThread project.

Establishing Priorities

In an application using multiple threads, it is important to know which threads will have a higher priority and run first. Table 3.5 describes all possible priority levels.

Table 3.5 Thread Priorities

Priority Level

Description

THREAD_PRIORITY_TIME_CRITICAL

15 points above normal

THREAD_PRIORITY_HIGHEST

2 points above normal

THREAD_PRIORITY_ABOVE_NORMAL

1 point above normal

THREAD_PRIORITY_NORMAL

Normal

THREAD_PRIORITY_BELOW_NORMAL

1 point below normal

THREAD_PRIORITY_LOWEST

2 points below normal

THREAD_PRIORITY_IDLE

15 points below normal


All threads are created using the THREAD_PRIORITY_NORMAL. After a thread has been created, you can adjust the priority level higher or lower using the SetThreadPriority() function. A general rule is that a thread dealing with the user interface should have a higher priority to make sure that the application remains responsive to the user's actions. Background threads are usually set to THREAD_PRIORITY_BELOW_NORMAL or THREAD_PRIORITY_LOWEST so that they can be terminated when necessary.

NOTE

Priority levels are commonly called relative scheduling priorities because they are relative to other threads in the same process.

The TThread object provides a Priority property, which determines the thread priority level. Its possible values are

tpTimeCritical
tpHighest
tpHigher
tpNormal
tpLower
tpLowest
tpIdle

As you can see, they closely match the priority levels you previously described.

If you're still not convinced of the importance of thread priorities, take a look at the following example. Start a new application and add two progress bars (Max property set to 5000) and a Start button. You will try to increment the position of the progress bars using threads of different priorities. Listing 3.17 contains the source code of the TPriorityThread thread object.

Listing 3.17 TPriorityThread Thread Object

#include <vcl.h>
#pragma hdrstop

#include "PriorityThreadUnit.h"
#include "ThreadFormUnit.h"
#pragma package(smart_init)

__fastcall TPriorityThread::TPriorityThread(bool
  Temp) : TThread(false)
{
  First = Temp;
}

void __fastcall TPriorityThread::DisplayProgress()
{
  if(First)
   ThreadForm->ProgressBar1->Position++;
  else
   ThreadForm->ProgressBar2->Position++;
}

void __fastcall TPriorityThread::Execute()
{
  FreeOnTerminate = true;
  for(Num = 0; Num <= 5000; Num++)
  {
   if(Terminated) break;
   Synchronize (DisplayProgress);
  }
}

Notice that I slightly modified the TPriorityThread constructor. The Temp boolean variable (which replaces CreateSuspended) will indicate which progress bar should be accessed.

The main unit contains only the code for the Start button OnClick handler:

void __fastcall TThreadForm::bStartClick(TObject *)
{
  TPriorityThread *First;
  First = new TPriorityThread (true);
  First->Priority = tpLowest;

  TPriorityThread *Second;
  Second = new TPriorityThread(false);
  Second->Priority = tpLowest;

  bStart->Enabled = false;
}

Run the program and click the Start button. Both progress bars should reach the end at approximately the same time, as shown in Figure 3.18. Now, set the priority of the first thread to tpLower. Any difference? See the result in Figure 3.19.

Figure 3.18Figure 3.18 Threads with same priority.

Figure 3.19Figure 3.19 Threads with different priorities.

Timing Threads

Sometimes when developing it is useful to time sections of code. The basic principle is to record the system time before and after the code and subtract the start time from the end time to calculate the elapsed time. For general applications this can be done with the Win32 API function GetTickCount(). This is illustrated in Listing 3.18.

Listing 3.18 Timing Code with GetTickCount()

  int Start = GetTickCount();

  // ...
  Form1->Canvas->Lock();
  for(int x = 0; x <= 100000; x++)
    Form1->Canvas->TextOut(10, 10, x);
  Form1->Canvas->Unlock();
  // ...

  int Total = GetTickCount() - Start;
  ShowMessage(FloatToStr(Total / 1000.0) + " sec");

A similar example could be created using the clock() instead of GetTickCount(). There are also other functions that can be used to time code.

Unfortunately, because of the preemptive behavior of Windows, threads are often interrupted. For this reason, you can't rely on GetTickCount() to retrieve the thread execution time. However, Windows provides the GetThreadTimes() API function, which helps you time your threads:

BOOL GetThreadTimes(
  HANDLE hThread,
  LPFILETIME lpCreationTime,
  LPFILETIME lpExitTime,
  LPFILETIME lpKernelTime,
  LPFILETIME lpUserTime
  );

WARNING

GetThreadTimes() is available only with Windows NT/2000.

As you can see, GetThreadTimes() uses the FILETIME structure. Before performing arithmetic operations, you must first store the user time information in a LARGE_INTEGER. Then, by subtraction of the 64-bit QuadPart members of the LARGE_INTEGER structure, you could obtain the number of 100 nanoseconds that your code takes to execute. Listing 3.19 illustrates this.

Listing 3.19 GetThreadTimes() Example

  FILETIME CreationTime, ExitTime, KernelTime;
  union {
   LARGE_INTEGER iUT;
   FILETIME fUT;
  } UserTimeS, UserTimeE;

  GetThreadTimes((HANDLE)Handle, &CreationTime,
   &ExitTime, &KernelTime, &UserTimeS.fUT);

  // ...
  Form1->Canvas->Lock();
  for(int x = 0; x <= 100000; x++)
   Form1->Canvas->TextOut(10, 10, x);
  Form1->Canvas->Unlock();
  // ...

  GetThreadTimes((HANDLE)Handle, &CreationTime,
   &ExitTime, &KernelTime, &UserTimeE.fUT);

  float Total = UserTimeE.iUT.QuadPart - UserTimeS.
   iUT.QuadPart;
  Total /= 10 * 1000 * 1000; // Converts to seconds

  OutputDebugString(FloatToStr(Total).c_str());

TIP

OutputDebugString() is a useful API function that sends a string to the Event Log debug window. Under normal circumstances, I have the tendency to use message boxes or to change the window caption, but in multithreaded applications these actions can sometimes be disastrous without considerable coding. OutputDebugString() is, therefore, a perfect alternative.

TIP

The OutputDebugString() function is covered in more detail in win32.hlp, which is part of the C++Builder installation in the Borland Shared\MSHelp directory.

Synchronizing Threads

Probably the greatest disadvantage of using threads is the difficulty in organizing them. Let's say your application is simultaneously running two threads, which modify some global data. What will happen if they try to access the same data at the same time? Or, what if the second thread has to wait for the first thread to process this data, and then execute? To coordinate threads, Windows offers various methods of synchronization.

Critical Sections

To illustrate two threads accessing the same global data, you'll create a sample application using the TCriticalThread object (see Listing 3.20).

Listing 3.20 CriticalThreadUnit.cpp: TCriticalThread Thread Object

#include <vcl.h>
#pragma hdrstop

#include "CriticalThreadUnit.h"
#include "ThreadFormUnit.h"

#pragma package(smart_init)

__fastcall TCriticalThread::TCriticalThread(bool CreateSuspended)
  : TThread(CreateSuspended)
{
}

void __fastcall TCriticalThread::DisplayList()
{
  ThreadForm->ListBox->Items->Add(Text);
}

void __fastcall TCriticalThread::Execute()
{
  FreeOnTerminate = true;

  for(int x = 0; x <= 50; x++)
  {
   if(Terminated) break;

   // EnterCriticalSection(&ThreadForm->CS);
   Sleep(50);
   ThreadForm->ListText.Insert("=====", 1);
   Text = ThreadForm->ListText;
   Synchronize(DisplayList);
   ThreadForm->ListText.SetLength(ThreadForm->
     ListText.Length() - 5);
   // LeaveCriticalSection(&ThreadForm->CS);
  }
}

And in your main unit, you'll create two instances of this object (see Listing 3.21).

Listing 3.21 ThreadFormUnit.cpp

#include <vcl.h>
#pragma hdrstop

#include "ThreadFormUnit.h"
#include "CriticalThreadUnit.h"

#pragma package(smart_init)
#pragma resource "*.dfm"

TThreadForm *ThreadForm;

__fastcall TThreadForm::TThreadForm(TComponent* Owner)
  : TForm(Owner)
{
  ListText = "=====";
  // InitializeCriticalSection(&CS);
}

void __fastcall TThreadForm::StartClick(TObject *Sender)
{
  TCriticalThread *FirstThread;
  FirstThread = new TCriticalThread(false);

  TCriticalThread *SecondThread;
  SecondThread = new TCriticalThread(false);
}

void __fastcall TThreadForm::FormClose(TObject *,
   TCloseAction &Action)
{
  // DeleteCriticalSection(&CS);
}

Your code is both simple and useless, but it will demonstrate the importance of thread synchronization. First, the TCriticalThread object adds to the global ListText variable five equals (=) characters. Then, it adds the value of ListText to a ListBox. Finally, TCriticalThread() truncates five characters, thus setting ListText to the value it initially had. Logically, all ListBox items should display ==========, but as Figure 3.20 shows, that's not always the case. Why? Because the second thread also accesses the same global variable.

Figure 3.20Figure 3.20 The CriticalThread project without critical sections.

Critical sections are an easy and efficient way to temporarily block other threads from accessing data (similar to the Lock() and Unlock() methods for graphic objects). To define a critical section, you'll use four basic API functions:

VOID InitializeCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
  );

VOID EnterCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
  );

VOID LeaveCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
  );

VOID DeleteCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
  );

It's not so difficult to guess how to use these functions. First, you declare a variable of type CRITICAL_SECTION. You initialize this variable at program startup (InitializeCriticalSection()) and delete it when the program closes (DeleteCriticalSection()). When your thread starts processing data, you block access to other threads with EnterCriticalSection() and, when it finishes, you exit the critical section (LeaveCriticalSection()).

Go back to Listings 3.20 and 3.21, and comment out the four lines, which call the critical section functions I described. Then, open the header file of your main unit and add the following line:

  CRITICAL_SECTION CS;

As shown on Figure 3.21, all ListBox items now contain the same string.

Figure 3.21Figure 3.21 The CriticalThread project with critical sections.

Mutexes

Mutexes offer the functionality of critical sections, while adding other interesting features.

TIP

Although featureless, critical sections are slightly faster then mutexes and semaphores. If time is an important factor in your application, consider using critical sections.

Mutex objects are created using the CreateMutex() API function:

HANDLE CreateMutex(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,
  BOOL bInitialOwner,
  LPCTSTR lpName
  );

After you have the handle of the newly created mutex object, you must use the WaitForSingleObject() function. This API call will request ownership for the mutex object, wait until this object becomes available, and use the mutex until ReleaseMutex() is called:

  HANDLE Mutex;
  Mutex = CreateMutex(NULL, false, NULL);
  if(Mutex == NULL)
  {
   ShowMessage("Cannot create mutex!");
   return;
  }

  // ...

  if(WaitForSingleObject(Mutex, INFINITE) ==
   WAIT_OBJECT_0)
  {
   // ...
  }

  ReleaseMutex (Mutex);

NOTE

Unlike critical sections, two or more processes can use the same mutex.

NOTE

If a thread doesn't release its ownership of a mutex object, this mutex is considered to be abandoned. Therefore, WaitForSingleObject() will return WAIT_ABANDONED. Although it's not perfectly safe, you can always acquire ownership of an abandoned mutex.

Others

Other synchronization objects such as semaphores and timers are also available. By familiarizing yourself with critical sections and mutexes, you'll already be one step ahead into mastering thread synchronization.

  • + Share This
  • 🔖 Save To Your Account

Discussions

comments powered by Disqus