Using Threads in Applications
Hello, everybody!
It was a long delay for all our team was really overloaded with work. Wow!
Since most of the applications we develop involve sockets for connecting to the Internet and other networks, time of connection is critical, for it may influence the productivity of the whole programme. If a connection has timed out, it shouldn’t hang up all the other processes. For instance, a user still must be able to change options, open/close files, type in commands etc.
So, in other words the application must do many things simultaneously.
Threads are designed for such cases. You may assign a separate thread, or a channel, or a portion of a computer’s resources, to practically each action that is executed within your project. But usually threads are provided for resource-eating tasks, for example complicated mathematical calculations, intensive and frequent operations with a data medium or SQL server, and of course for socket connections too.
Threads remind OnTimer events: at certain intervals an OnTimer event starts execution, and no matter has it finished or not, at the next interval the same event will occur again, and they’ll work simultaneously.
I will describe an example application written in C++ (in Broland C++ Builder IDE) that has the following resource-consuming functions:
1. Connecting to a remote server and receiving a session ID 2. Querying a server for the list of products 3. Getting account balance
First, you must create a regular C++ project. I'll call the main unit main.cpp. Of course your project may already contain many other units.
Then I'll create a new unit that will do all functions 1-3. But this is a special unit, so press “File/New/Other” to open the Borland ‘New Items” wizard (Fig. 1)
Figure 1 - "New Items" C++ Builder Wizard
Choose “Thread Object” and type in the name of the thread class. I chose TMyConnector. A new unit for the class definition and implementation will be created.
Right now it consists only of two functions: the constructor TMyConnector and the most important function of the class, called Execute(). The constructor of the thread can be used for initializing the class’ properties. Don’t forget that a thread is very much like other classes, and you can access its properties and methods.
Execute() function is launched after initialization and is used for doing all the time- and resource-consuming things that I talked about before. You may insert the whole procedure into Execute() function or call other functions and procedures from Execute().
The only strict condition for the code that is executed within Execute() is that it by no means can change properties of VCL components and variables outside the thread and objects that it creates. This is a way to provide data integrity. Once you launched a thread, the application can proceed without even knowing what’s going on there. A user may change input, press buttons, toggle flags and if several threads access this part of memory, they may change it occasionally and conflict with each other and the main application.
Diagram 1 shows the principles of working with a thread.
 Diagram 1 - How applications and threads interact.
Now, let’s define our server-related functions in TMyConnector class:
myconnector.h:
typedef enum TConnectorType { FGetSession, FProductList, FGetFunds } none;
class TMyConnector : public TThread { public: TConnectorType TYPE; …. } |
I defined a type of a possible function that my thread can execute. According to the Diagram 1, they are Function 1, Function 2 …Function N, and they will get the necessary data, process it and return back to the thread.
Now I need to fill in Execute() function:
myconnector.cpp: void __fastcall TMyConnector::Execute() { switch (TYPE) { case (TConnectorType)FGetSession: /// getting a session Session = GetSession(URL); Synchronize(SessionGot); break; case (TConnectorType)FProductList: /// getting a product list ProductList = GetProducts(URL); Synchronize(ProductsGot); break; case (TConnectorType)FGetFunds: /// getting account balance Funds=GetFunds (URL); Synchronize(FundsGot); break; default: Synchronize(ShowError); // if an unknown function was called, we issue an error } } |
Functions GetSession, GetProducts and GetFunds may be declared outside MyConnector unit. They must not access VCL components of any form, and variables that were not created by them or MyConnector. They connect to the server via sockets
In order to allow these functions to interact with MyConnector, I created several properties of this object: Session, ProductList, Funds and URL. I declared them previously in myconnector.h:
myconnector.h
class TMyConnector : public TThread { public: AnsiString Session; TStringList* ProductList; float Funds; AnsiString URL; …. } |
We will initialize URL before starting the thread. I’ll tell about it later. We will get all the necessary data from the function working with socket connections into the above variables, and then will pass them to the Synchronize function.
Synchronize function waits for the main VCL processes to be available for changing and then executes the passed method. The method should belong to the thread object (MyConnector in this case). Actually this only makes sense if your thread functions want to change the VCL objects in the main application (anything that is not thread-aware).
Synchronize doesn’t work with console application, because they don’t contain VCL components.
Omitting Synchronize improves performance, because you don’t need to wait until VCL objects become available for changing.
For instance we got a session from the server, and want to display it in an Edit field somewhere in a form. If a user types something into that Edit at the time you’ve got the session, you’ll have to wait.
You can create as many methods of the thread object as you want, and pass them to Synchronize as a command to execute these methods after the thread completed its work. Methods that are passed to Synchronize do not have any limits as for the objects they may access. And of course they can access the properties of the thread itself.
So, I added them to the definition of MyConnector:
myconnector.h
class TXParser : public TThread { private: void __fastcall SessionGot(void); void __fastcall ProductsGot(void); void __fastcall FundsGot(void); ... } |
myconnector.cpp
void __fastcall TXParser::SessionGot(void) { MainForm->Edit1->Text=Session; /// Session is displayed in the main form } |
Finally, after we defined the thread functions, we should call it from the main application.
Threads are created as any other objects:
main.cpp
void __fastcall TMainForm::Button1Click(TObject *Sender) { TMyConnector *MyConnector = new TMyConnecotr(true); … } |
The true parameter passed to the constructor means that the thread is created “suspended”. That is, it won’t start until a special command is given. This is useful if you want to initialize some properties of the thread before launching it.
In our case, we must define the type of the function we want to execute in the thread, and also the URL of the server. Then everything’s ready and we launch the thread with a special method Resume():
main.cpp
void __fastcall TMainForm::Button1Click(TObject *Sender) { TMyConnector *MyConnector = new TMyConnecotr(true); MyConnector->TYPE = PGetSession; MyConnector->URL =”www.example.com”; MyConnector->Resume(); .. /* you may do other calculations and actions here. They’ll be launched immediately after Resume() starts, and won’t wait for the thread to complete. */ } |
But what’s the sense of threads if you have to wait for them?
Remember that if you check MainForm->Edit1->Text using a debugger (breakpoint set at the line next to MyConnector->Resume()), you may find that it hasn’t changed yet. If you want to complete the thread and only proceed afterwards, you should replace Resume() with WaitFor().
In this case the application will wait for the results of the thread before proceeding.
You may create as many thread objects as you want. For instance, in the example above you might write:
main.cpp
void __fastcall TMainForm::Button1Click(TObject *Sender) { TMyConnector *MyConnector = new TMyConnecotr(true); MyConnector->TYPE = PGetSession; MyConnector->URL =”www.example.com”; MyConnector->Resume();
TMyConnector *MyConnector1 = new TMyConnecotr(true); MyConnector1->TYPE = PGetFunds; MyConnector1->URL =”www.example.com”; MyConnector1->Resume();
TMyConnector *MyConnector2 = new TMyConnecotr(true); MyConnector2->TYPE = PGetProducts; MyConnector2->URL =”www.example.com”; MyConnector2->Resume(); } |
Be careful however. Always check how many threads are launched by your application at a time, or a user’s computer will hang.
Happy programming threads!
|