Hello people,
In general, is it possible in a TThread.Execute procedure to call a TDataModule method, in which there is no visual activity involved?
Thanks to all, Massimo.
Hello people,
In general, is it possible in a TThread.Execute procedure to call a TDataModule method, in which there is no visual activity involved?
Thanks to all, Massimo.
Short answer: yes
Long answer: The problem with Windows is that all the GUI activity should be done in a single thread. (Well, the above statement can be expanded, amended, enhanced etc. but for our discussion is enough). So, if you are sure that in your TDataModule method there isn't any 'GUI thing' involved (beware, this can be even a ShowMessage
call) then go ahead.
UPDATE: Of course, there are techniques to update your GUI from a secondary thread, but this implies some sort of preparation (message passing, Synchronize
etc.). Isn't something very hard, just that you cannot 'blindly' call from another thread a method who changes the GUI.
To use our industries favorite answer when asked anything: It depends.
If you have a method on your datamodule that is completely self contained (ie could be a static method), you shouldn't have any problem.
Example
TMyDataModule = class(TDataModule)
public
function AddOne(const Value: Integer): Integer;
end;
function TMyDataModule.AddOne(const Value: Integer): Integer;
begin
Result := Value + 1;
end;
If on the other hand, the method uses any global state, you might get into trouble when calling it from multiple threads.
Example
TMyDataModule = class(TDataModule)
private
FNumber: Integer
public
function AddOne(const Value: Integer): Integer;
end;
function TMyDataModule.AddOne(const Value: Integer): Integer;
begin
FNumber := Value
//***** A context switch here will mess up the result of (at least) one thread.
Result := FNumber + 1;
end;
Global state should be interpreted very wide. A TQuery, a TTable, refreshing the GUI, using any global variable, ... is all global state and isn't thread safe.
The easiest way to go is to use TThread.Synchronize to invoke a method in your data module.
However, if you do not wish to do that, even when no visual activity is involved, you should determine whether or not you need to add a critical section to protect you.
Any access to any standard or third-party VCL component, whether it is visual (TButton) or non-visual (datasets) should be considered UNSAFE. Any access to a local data object (like a private field or global variable) must also be protected by critical sections.
Here's a direct call from a from background thread to your data module:
if Assigned(MyDataModule) then MyDataModule.DoSomething(a,b,c);
Here's the code in your data module, which I am showing you a sample bit of code that makes sure that we are the only thread touching FList right now:
/// DoSomething: Note this method must be thread-safe!
procedure TMyDataModule.DoSomething(a:TMyObject1;b:TMyObject2;c:TMyObject3);
begin
FCriticalSection.Enter;
try
if not FList.Contains(a) then
FList.Add(a);
...
finally
FCriticalSection.Leave;
end;
end;
/// elsewhere in the same data module, wherever anybody modifies or checks the state
/// (content) of FList, wrap the method with a critical section like this:
function TMyDataModule.HasItem(a:TMyObject1):Boolean;
begin
FCriticalSection.Enter;
try
result := FList.Contains(a);
finally
FCriticalSection.Leave;
end;
end;
Some starter rules for Delphi multi-threaded programming, in a nutshell are:
Like Lieven writes, it depends.
If you have database components on the datamodule, you has to know if the are thread safe, og har to make them threadsafe.
Some database components requires a seperate session object pr. thread.
Yes, my question is very vague.
My program is a graphical statistics app, it has to display Gantt chart, by means of TChart, describing the states, alarms or machined orders of one or more Tool Machine. On the supervisor PC a server (equipped with a TIdTcpServer and some DB components) is listening to my app on the LAN.
The main-form client allows the final user to choice a range of dates (period) and the units (machines) to query the server. After that, the user press a button (there are 3 functionalities): a new form (and Datamodule) is created to display the results.
The work of collecting data is completed by a thread because:
1) it can be a long job so it could freeze the GUI;
2) the user can launch more than one form to see various results.
I have a basic Datamodule (with a TIdTcpClient with several function to collect the data), a basic form (never instantiated, with a lot of characteristics common to all data form, and the definition of the worker thread).
unit dtmPDoxClientU;
TdtmPDoxClient = class(TDataModule)
IdTCPClient: TIdTCPClient;
...
function GetData(...): boolean;
...
end;
unit frmChartBaseFormU;
TfrmChartBaseForm = class(TForm)
...
TheThread: TThreadClient;
procedure WMThreadComm(var Message: TMessage); message WM_THREADCOMM;
procedure ListenThreadEvents(var Message: TMessage); virtual;
procedure ExecuteInThread(AThread: TThreadClient); virtual;
end;
TThreadClient = class(TThread)
private
public
Task: integer;
Module: TfrmChartBaseForm;
procedure Execute; override;
property Terminated;
end;
procedure TfrmChartBaseForm.FormCreate(Sender: TObject);
...
TheThread := TThreadClient.Create(true);
with TheThread do begin
Module := self;
FreeOnTerminate := true;
end;//with
end;//FormCreate
procedure TfrmChartBaseForm.WMThreadComm(var Message: TMessage);
begin
ListenThreadEvents(Message);
end;//WMThreadComm
procedure TfrmChartBaseForm.ListenThreadEvents(var Message: TMessage);
begin
// do override in derived classes
end;//ListenThreadEvents
procedure TfrmChartBaseForm.ExecuteInThread(AThread: TThreadClient);
begin
// do override in derived classes
end;//ExecuteInThread
procedure TThreadClient.Execute;
begin
with Module do begin
ExecuteInThread(self);
end;//with
end;//Execute
Furthermore, using VFI, I also have two units:
unit dtmPDoxClientDataOIU;
TdtmPDoxClientDataOI = class(TdtmPDoxClient)
cdsClient_IS: TClientDataSet;
...
dsr_I: TDataSource;
...
private
public
end;
unit frmPDoxClientDataOIU;
TfrmPDoxClientDataOI = class(TfrmChartBaseForm)
ChartOI: TChart;
...
procedure FormCreate(Sender: TObject);
public
{ Public declarations }
dtmPDoxClientDataOI: TdtmPDoxClientDataOI;
procedure ListenThreadEvents(var Message: TMessage); override;
procedure ExecuteInThread(AThread: TThreadClient); override;
end;
procedure TfrmPDoxClientDataOI.FormCreate(Sender: TObject);
begin
inherited;
dtmPDoxClientDataOI := TdtmPDoxClientDataOI.Create(self);
TheThread.Task := 1;
TheThread.Resume;
end;//FormCreate
procedure TfrmPDoxClientDataOI.ListenThreadEvents(var Message: TMessage);
begin
if (Message.WParam = 1) then begin
case Message.LParam of
//GUI tasks, using ClientDataset already compiled and not re-used
end;//case
end;//if
end;//ListenThreadEvents
procedure TfrmPDoxClientDataOI.ExecuteInThread(AThread: TThreadClient);
begin
while not AThread.Terminated and (AThread.Task <> 0) do begin
case AThread.Task of
1: begin
if dtmPDoxClientDataOI.GetData(...) then
if not AThread.Terminated then begin
PostMessage(Handle,WM_THREADCOMM,1,1);
AThread.Task := 2;
end //if
else
AThread.Task := 0;
end;//1
... etc...
end;//case
end;//while
end;//ExecuteInThread
So, when the final user presses the button, a new form and its own datamodule and thread are created; the thread uses its own datamodule by means of ExecuteInThread function. When data are ready, a PostMessage is sent to the form, which updates the chart.