欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

delphi 四种创建线程的方式及对比

程序员文章站 2022-03-03 18:42:31
...

Last time (in issue #9) I wrote in length about parallel execution and multithreading but I never wrote any code. It was all just talk, talk, talk. Sorry for that, but I felt I had to write a solid introduction before delving into murky waters of multithreading.

Today I intend to fix that. This article will focus more on code than words. We still won't write even a half-serious program, though. Instead of that, I'll show you how complex even a simple task of creating a new thread can be. It is not complex because of the lack of tools, oh no, quite contrary. It is complex because there are many tools that can be used, each with its own set of advantages and disadvantages.

The Delphi Way

In the first place I'll cover the most obvious approach – Delphi's own threading tools.

Creating a thread in Delphi is as simple as declaring a class that descends from the TThread class (which lives in the Classes unit), overriding its Execute method and instantiating an object of this class (in other words, calling TMyThread.Create). Sounds simple, but the devil is, as always, in the details.

TMyThread=class(TThread)
protected
procedureExecute;override;
end;

FThread1:=TMyThread1.Create;

Before we start, I should put out a word of warning. TThread has changed quite a lot since Delphi 2. I tried to use only most common functionality, which is still mostly unchanged in Delphi 2010, but it is entirely possible that the code would not work correctly in the oldest Delphi releases.. The code was tested with Delphi 2007 and if you find any problems with the code in some other Delphi, then let me know so I can fix it in one of future instalments of the Threading series.

The code archive for this article includes application TestTThread, which demonstrates two most obvious thread patterns. The first is a thread that does repeating work. Owner starts it, leaves it running for as long as it is needed (often this is as long as the application is running), then stops the thread. The second is a thread that does one unit of work, then notifies the owner of the results and stops.

An example of the first pattern would be a background polling thread. During its execution such thread monitors some resource (often by doing some test followed by a short sleep) and notifies the owner then the resource is updated. Often the thread would also do the actual handling of the resource. The second pattern covers many lengthy operations, that can be executed in the background, for example copying of large file or uploading a file to the web server.

Let's return to the sample code. In the first case, user starts the thread by clicking the „Start thread 1“ button, which executes the following code (slightly simplified; actual code in the project also does some logging so you can see in the GUI what's going on):

procedureTfrmTestTThread.btnStartThread1Click(Sender:TObject);
begin
FThread1:=TTestThread1.Create(false);
btnStartThread1.Enabled:=false;
btnStopThread1.Enabled:=true;
end;

The code first creates an instance of the TTestThread1 class (which I'll present in a moment). The parameter false instructs the thread constructor that it can immediately start the new thread. The code then disables the Start button and enables the Stop button.

The code for stopping the thread is just a tad more complicated.

procedureTfrmTestTThread.btnStopThread1Click(Sender:TObject);
begin
FThread1.Terminate;
FThread1.WaitFor;
FreeAndNil(FThread1);
btnStartThread1.Enabled:=true;
btnStopThread1.Enabled:=false;
end;

First the code calls thread's Terminate method which instructs the thread to terminate (and again we'll ignore the mechanism behind this for a moment). Then it waits on the thread to terminate and destroys the thread object.

Usually, you'll not be using this long version. It's equally well if you just destroy the thread object because the destructor (TThread.Destroy) will automatically execute Terminate and WaitFor for you.

Finally, let's take a look at the TTestThread1 code. As you may expect, the class itself descends from the TThread class and implements overridden Execute method.

type
TTestThread1=class(TThread)
strictprivate
FMsg:string;
protected
procedureExecute;override;
procedureLog;
end;

procedureTTestThread1.Execute;
begin
FMsg:=Format('Thread %d started',[ThreadID]);
Synchronize(Log);
whilenotTerminateddobegin
// some real work could be done here
Sleep(1000);
FMsg:=Format('Thread %d working ...',[ThreadID]);
Synchronize(Log);
end;
FMsg:=Format('Thread %d stopping ...',[ThreadID]);
Synchronize(Log);
end;

procedureTTestThread1.Log;
begin
frmTestTThread.Log(FMsg);
end;

In Execute, thread first signals the owner that is has commenced execution (first two lines of the method) and then enters the thread work cycle: check if owner has requested termination, do some real work, sleep for a short time. During the execution it will also report the current state to the owner.

Although I wanted to skip all dirty details today, I was only partially successful. I wanted threads in the demo code to send the execution state to the owner and that is, believe it or not, always a messy thing. The code above uses a Synchronize approach. This method executes another method (which is its parameter, Log in this case) to be executed in the context of the main VCL thread. In other words, when you call Synchronize, background thread (TTestThread1) will pause and wait for the parameter method (Log) to be executed in the main program. Then the execution of the background thread will resume. As the parameter method cannot have any parameters I had to put the log message into a class field called FMsg.

Let me emphasize two points here. Firstly, the Synchronize is the only way to safely execute VCL code from the background thread! VCL is not thread-safe and expects to be used only from the main thread! Don't call VCL (and that includes all GUI manipulation) directly from the background thread! If you do this, your code may seem to work but you'll introduce hard to find problems that will sometimes crash your program.

Secondly, I disagree with Synchronize deeply. Its use should be severely limited. Heck, it should not be documented at all. It shouldn't even exist! There are better ways to decouple background threads from the GUI and one of them I'll use later in this article.

Let's move to the pattern no. 2. The code to start the thread is similar to the one we've already seen.

procedureTfrmTestTThread.btnStartThread2Click(Sender:TObject);
begin
FThread2:=TTestThread2.Create(true);
FThread2.FreeOnTerminate:=true;
FThread2.OnTerminate:=ReportThreadTerminated;
FThread2.Resume;
btnStartThread2.Enabled:=false;
end;

The code again creates the thread object, but this time true is passed for the CreateSuspended parameter and the thread will be created in suspended state. In other words, the thread object will be created, but associated operating system thread will be paused.

The code then instructs the FThread2 to automatically destroy itself when the Execute method completes its work and sets the OnTerminate handler, which will be called when the thread will be terminated. At the end it calls Resume to start the thread.

Another word of warning – this is the only legitimate way of using Resume. Don't ever call Suspend to pause a thread and Resume to resume it! You'll only cause havoc. Actually, you're not supposed to use Resume in Delphi 2010 anymore. Its use has become deprecated and it was replaced with the Start method, which is a Resume with all the evil parts removed; only the code that does good was left.

As this is a one-shot operation, there is no code to stop the thread. Instead, the thread's Execute sleeps a little to indicate some very hard work and then exits.

procedureTTestThread2.Execute;
begin
FMsg:=Format('Thread %d started',[ThreadID]);
Synchronize(Log);
FMsg:=Format('Thread %d working ...',[ThreadID]);
Synchronize(Log);
// some real work could be done here
Sleep(5000);
FMsg:=Format('Thread %d stopping ...',[ThreadID]);
Synchronize(Log);
end;

When the Execute completes its work, TThread infrastructure calls the OnTerminate event handler where the main GUI thread can update its status.

procedureTfrmTestTThread.ReportThreadTerminated(Sender:TObject);
begin
Log(Format('Thread %d terminated',[TThread(Sender).ThreadID]));
btnStartThread2.Enabled:=true;
end;

Until the next time, that is all I have to say about TThread and threading in VLC. If you want to know more, read theexcellent tutorial by Martin Harvey.

The Windows Way

Surely, the TThread class is not complicated to use but the eternal hacker in all of us wants to know – how? How is TThread implemented? How do threads function at the lowest level. It turns out that the Windows' threading API is not overly complicated and that it can be easily used from Delphi applications.

It's easy to find the appropriate API, just look at the TThread.Create. Besides other things it includes the following code (Delphi 2007):

FHandle:=BeginThread(nil,0,@ThreadProc,Pointer(Self),CREATE_SUSPENDED,FThreadID);
ifFHandle=0then
raiseEThread.CreateResFmt(@SThreadCreateError,[SysErrorMessage(GetLastError)]);

If we follow this a level deeper, into BeginThread, we can see that it calls CreateThread. A short search points out that this is a Win32 kernel function, and a look into the MSDN confirms that it is indeed a true and proper way to start a new thread.

Let's take a look at the declaration and step through the parameters.

functionCreateThread(lpThreadAttributes:Pointer;
dwStackSize:DWORD;lpStartAddress:TFNThreadStartRoutine;
lpParameter:Pointer;dwCreationFlags:DWORD;varlpThreadId:DWORD):THandle;stdcall;
  • lpThreadAttributes is a pointer to security attributes structure. You'll probably never have to use it so just use nil.
  • dwStackSize is the initial stack size, in bytes. If you set it to zero, default stack size (1 MB) will be used. This is what Delphi's BeginThread does.
  • lpStartAddress is the address of the method that will start its life in the new thread.
  • lpParameter is arbitrary data that will be passed to the thread. We can use it to pass configuration parameters to the thread code.
  • dwCreationFlags contains flags that govern thread creation and behaviour. Of particular importance here is the CREATE_SUSPENDED flag which will the thread to be created in suspended (not running) state.
  • lpThreadID is output (var) parameter that will receive the thread's ID. Each thread in the system has unique identifier associated with it and we can use this identifier in various API functions.

The result of the CreateThread call is a handle to the thread. This is a value that has no external value (does not give you any knowledge by itself) but can again be passed to various API functions. If the CreateThread fails, the result will be 0.

The testWinAPI program demonstrates the use of Win32 API for thread creation.  Again, it contains two test cases – one running a perpetual thread and another a one-shot thread.

In first case, the thread creation code is quite simple – just a call to CreateThread and a safety check. In the lwParameter field it is passing an address of a boolean field which will be set to True to stop the thread.

procedureTfrmTestWinAPI.btnStartThread1Click(Sender:TObject);
begin
FStopThread1:=false;
FThread1:=CreateThread(nil,0,@ThreadProc1,@FStopThread1,0,FThread1ID);
ifFThread1=0then
RaiseLastOSError;// RaiseLastWin32Error in older Delphis
btnStartThread1.Enabled:=false;
btnStopThread1.Enabled:=true;
end;

Thread method is not terribly complicated either. The most important thing is that it is declared as a function of one pointer parameter (in my case I specifically declared this parameter as a pointer to Boolean but any pointer type would do) returning a DWORD (or cardinal, if you want) and with the stdcall flag attached.

functionThreadProc1(stopFlag:PBoolean):DWORD;stdcall;
begin
PostMessage(frmTestWinAPI.Handle,WM_THREAD_INFO,
MSG_THREAD_START,GetCurrentThreadID);
whilenotstopFlag^dobegin
// some real work could be done here
Sleep(1000);
PostMessage(frmTestWinAPI.Handle,WM_THREAD_INFO,
MSG_THREAD_WORKING,GetCurrentThreadID);
end;
PostMessage(frmTestWinAPI.Handle,WM_THREAD_INFO,
MSG_THREAD_STOP,GetCurrentThreadID);
Result:=0;
end;

There are two main differences between this method and the TThread version. Firstly, this thread is stopped by setting a FStopThread1 flag to True. The thread received a pointer to this variable and can constantly check its contents. When the variable is True, the thread procedure exits and that stops the thread.

Secondly, we cannot use Synchronize as it is a method of the TThread class. Instead of that the thread is sending messages to the main form. In my opinion, this is far superior option as it doesn't block the thread. Besides that, it draws a line between the thread and main GUI responsibilities.

The main program declares message method WMThreadInfo to handle these messages. If this is the first time you encountered message-handling methods, just take a look at the code. It is very simple.

To stop the thread, the code first sets the stop flag to True and then waits on the thread handle to become signalled. Big words, I know, but they represent a very simple operation – a call to WaitForSingleObject API. As the second parameter to this call is INFINITE, it will wait until the thread terminates itself by exiting out of the ThreadProc1 function. Then the code calls CloseHandle on the thread handle and with that releases all internal resources held by the Windows. If we would skip this step, a small resource leak would be introduced at this point.

procedureTfrmTestWinAPI.btnStopThread1Click(Sender:TObject);
begin
FStopThread1:=true;
WaitForSingleObject(FThread1,INFINITE);
CloseHandle(FThread1);
btnStartThread1.Enabled:=true;
btnStopThread1.Enabled:=false;
end;

The creation code for the second test is similar, with one change – it demonstrates the use of CREATE_SUSPENDED flag.

procedureTfrmTestWinAPI.btnStartThread2Click(Sender:TObject);
begin
FMainHandle:=Handle;
FThread2:=CreateThread(nil,0,@ThreadProc2,@FMainHandle,
CREATE_SUSPENDED,FThread2ID);
ifFThread2=0then
RaiseLastOSError;// RaiseLastWin32Error in older Delphis
ResumeThread(FThread2);
btnStartThread2.Enabled:=false;
end;

As the thread is created in the suspended state, the code has to call ResumeThread API to start its execution. The termination code for the second example is very similar to the first one – just look it up in the code.

One more thing has to be said about the Win32 threads – why to use them at all? Why go down to the Win32 API if the Delphi's TThread is so more comfortable to use? I can think of two possible answers.

Firstly, you would use Win32 threads if working on a multi-language application (built using DLLs compiled with different compilers) where threads objects are passed from one part to another. A rare occasion, I'm sure, but it can happen.

Secondly, you may be creating lots and lots of threads. Although that is not really something that should be recommended, you may have a legitimate reason to do it. As the Delphi's TThread uses 1 MB of stack space for each thread, you can never create more than (approximately) 2000 threads. Using CreateThread you can provide threads with smaller stack and thusly create more threads – or create a program that successfully runs in a memory-tight environment. If you're going that way, be sure to read great blog post by Raymond Chen.

The Lightweight Way

From complicated to simple … There are many people on the Internet who thought that Delphi's approach to threading is overly complicated (from the programmer's viewpoint, that it). Of those, there are some that decided to do something about it. Some wrote components that wrap around TThread, some wrote threading libraries, but there's also a guy that tries to make threading as simple as possible. His name is Andreas Hausladen (aka Andy) and his library (actually it's just one unit) is called AsyncCalls and can be found at http://andy.jgknet.de/blog/?page%5Fid=100.

AsyncCalls is very generic as it supports all Delphis from version 5 onwards. It is licensed under the Mozilla Public License 1.1, which doesn't limit the use of AsyncCalls inside commercial applications. The only downside is that the documentation is scant and it may not be entirely trivial to start using AsyncCalls for your own threaded code. Still, there are some examples on the page linked above. This article should also help you started.

To create and start a thread (there is no support for creating threads in suspended state), just call AsyncCall method and pass it the name of the main thread method. The code below was taken from the demo testAsyncCalls:

procedureTfrmTestAsyncCalls.btnStartThread1Click(Sender:TObject);
begin
FStopThread1:=false;
FThreadCall1:=AsyncCall(ThreadProc1,integer(@FStopThread1));
Log('Started thread');// AsyncCalls threads have no IDs
btnStartThread1.Enabled:=false;
btnStopThread1.Enabled:=true;
end;

As you can see, we can send a parameter to the ThreadProc1. AsyncCalls defines many overloads for the AsyncCall method but none of them supports pointer type so I had to cheat and wrap my pointer into an integer. (Which is a practice that will cause ugly crashes after we get 64-bit Delphi compiler but … c'est la vie.) We can also use methods with variable number of parameters (array of const – as in the built-in Format function).

Thread code, on the other hand, is not as simple as we would expect it. Then main problem here is that I choose to use Andy's asynchronous version of TThread.Synchronize. (Asynchronous means that it just schedules the code to be executed in the main thread in the near future and continues immediately.) The problem here is that this LocalAsyncVclCall supports only local procedures. In other words, even if we have a form method that implements exactly the functionality we need, we cannot call it directly. The only way is to call a local procedure which then calls the desired method of the owning class. In the code below, LocalAsyncVclCall schedules (local) ReportProgress to be executed. ReportProgress then forwards the parameter to form's ReportProgress which shows the message on the screen.

procedureTfrmTestAsyncCalls.ReportProgress(varparam:integer);
begin
// show progress on screen
end;

procedureTfrmTestAsyncCalls.ThreadProc1(stopFlagInt:integer);
var
stopFlag:PBoolean;

procedureReportProgress(param:integer);
begin
frmTestAsyncCalls.ReportProgress(param);
end;

begin
stopFlag:=PBoolean(stopFlagInt);
LocalAsyncVclCall(@ReportProgress,MSG_THREAD_START);//async
whilenotstopFlag^dobegin
// some real work could be done here
Sleep(1000);
LocalAsyncVclCall(@ReportProgress,MSG_THREAD_WORKING);
end;
LocalAsyncVclCall(@ReportProgress,MSG_THREAD_STOP);
end;

We could also use message passing technique, just like in the Windows example above, but I wanted to show some of the AsyncCalls capabilities.

In the second test (one-shot thread) I've used a similar approach to signalize thread completion. This time the blocking version is used. This call works exactly as Delphi's Synchronize except that it can again call only local procedures.

procedureTfrmTestAsyncCalls.ThreadProc2(handle:integer);

procedureFinished;
begin
frmTestAsyncCalls.Finished;
end;

begin
// some real work could be done here
Sleep(5000);
LocalVclCall(@Finished);// blocking
end;

In the release 2.9 of AsyncCalls Andy added support for new language constructs in Delphi 2009 – generics and anonymous methods. The latter allows us to simplify the code greatly (demo testAsyncCalls2009).

procedureTfrmTestAsyncCalls.ThreadProc1(stopFlagInt:integer);
var
stopFlag:PBoolean;
begin
stopFlag:=PBoolean(stopFlagInt);
TAsyncCalls.VCLInvoke(procedurebegin
ReportProgress(MSG_THREAD_START);end);
whilenotstopFlag^dobegin
// some real work could be done here
Sleep(1000);
TAsyncCalls.VCLInvoke(procedurebegin
ReportProgress(MSG_THREAD_WORKING);end);
end;
TAsyncCalls.VCLInvoke(procedurebegin
ReportProgress(MSG_THREAD_STOP);end);
end;

As you can see in the code above, the new VCLInvoke global method allows execution of anonymous procedure which then in turn calls the logging method.

The same technique can be used to write the thread code. Instead of writing a separate method that executes in a background thread, you can put all this code into an anonymous procedure and pass it to the Invoke.

procedureTfrmTestAsyncCalls.btnStartThread2Click(Sender:TObject);
begin
TAsyncCalls.Invoke(procedurebegin
TAsyncCalls.VCLInvoke(procedurebeginReportProgress(MSG_THREAD_START);end);
TAsyncCalls.VCLInvoke(procedurebeginReportProgress(MSG_THREAD_WORKING);end);
// some real work could be done here
Sleep(5000);
TAsyncCalls.VCLInvoke(procedurebeginReportProgress(MSG_THREAD_STOP);end);
TAsyncCalls.VCLSync(procedurebeginFinished;end);
end);
btnStartThread2.Enabled:=false;
end;

AsyncCalls is a great solution to many threading problems. As it is actively developed, I can only recommend it.

The No-Fuss Way

I could say that I left the best for the end but that would be bragging. Namely, the last solution I'll describe is of my own making. Yep, it's all mine, my precioussssssss … (Please, don't run away! I'll stop emotional outbursts now. It's a promise.)

OmniThreadLibrary (OTL for short) approaches the threading problem from a different perspective. The main design guideline was: “Enable the programmer to work with threads in as fluent way as possible.” The code should ideally relieve you from all burdens commonly associated with multithreading. I'm the first to admit that the goal was not reached yet, but I'm slowly getting there.

The bad thing is that OTL has to be learned. It is not a simple unit that can be grasped in an afternoon, but a large framework with lots of functions. On the good side, there are many examples (http://otl.17slon.com/tutorials.htm; you'll also find download links there). On the bad side, the documentation is scant. Sorry for that, but you know how it goes – it is always more satisfying to program than to write documentation. Another downside is that it supports only Delphi 2007 and newer. OTL is released under the BSD license which doesn't limit you from using it in commercial applications in any way.

OTL is a message based framework and uses custom, extremely fast messaging system. You can still use any blocking stuff and write TThread-like multithreading code, if you like. Synchronize is, however, not supported. Why? Because I think it's a bad idea, that's why.

In OTL you don't create threads but tasks. A task can be executed in a new thread (as I did in the demo program testOTL) or in a thread pool. As the latter is not really a beginner level topic I won't cover it today.

procedureTfrmTestOTL.btnStartThread1Click(Sender:TObject);
begin
FThread1:=CreateTask(ThreadProc1).OnMessage(ReportProgress).Run;
Assert(assigned(FThread1));
btnStartThread1.Enabled:=false;
btnStopThread1.Enabled:=true;
end;

A task is created using CreateTask, which takes as a parameter a global procedure, a method, an instance of TOmniWorker class (or, usually, a descendant of that class) or an anonymous procedure (in Delphi 2009 and newer). In this example, a method from class TfrmTestOTL is used. CreateTask returns an interface, which can be used to control the task. As (almost) all methods of this interface return Self, you can chain method calls in a fluent way. The code fragment above uses this approach to declare a message handler (a method that will be called when the task sends a message to the owner) and then starts the task. In OTL, a task is always created in suspended state and you have to call Run to activate it.

The thread procedure uses the fact that OTL infrastructure automatically creates a messaging channel between the background thread and its owner and just sends notifications over this channel. Messages are processed in the main thread by the ReportProgress method.

procedureTfrmTestOTL.ThreadProc1(consttask:IOmniTask);
begin
task.Comm.Send(MSG_THREAD_START);
whilenottask.Terminateddobegin
// some real work could be done here
Sleep(1000);
task.Comm.Send(MSG_THREAD_WORKING);
end;
task.Comm.Send(MSG_THREAD_STOP);
end;

procedureTfrmTestOTL.ReportProgress(consttask:IOmniTaskControl;constmsg:
TOmniMessage);
begin
// log the message ...
end;

A similar approach is used in the one-shot thread except that it also declares OnTerminate handler which is called just before the background task object is destroyed.

procedureTfrmTestOTL.btnStartThread2Click(Sender:TObject);
begin
FThread2:=CreateTask(ThreadProc2)
.OnMessage(ReportProgress)
.OnTerminated(Thread2Terminated);
Assert(assigned(FThread2));
FThread2.Run;// just for demo
btnStartThread2.Enabled:=false;
end;

Similar to AsyncCalls, OTL supports anonymous procedures at various places. You can use one as a main thread procedure, or in the OnMessage and OnTerminated handlers. For example, in demo testOTL2009 an anonymous method is used to report task termination.

procedureTfrmTestOTL.btnStartThread2Click(Sender:TObject);
begin
FThread2:=CreateTask(ThreadProc2)
.OnMessage(ReportProgress)
.OnTerminated(procedure(consttask:IOmniTaskControl)begin
Log(Format('Thread %d terminated',[task.UniqueID]));
FThread2:=nil;
btnStartThread2.Enabled:=true;
end);
Assert(assigned(FThread2));
FThread2.Run;// delayed, just for demo
btnStartThread2.Enabled:=false;
end;

The OTL is being actively developed. For example, the next release (which will probably be released before this article is printed) will support higher-level control structures such as parallel for statement. Follow my blog if you want to stay informed.