views:

270

answers:

6

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.

A: 

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.

A: 

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.

Lieven
+1  A: 

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:

  • Don't do anything that could create a Race Condition.
  • Don't forget to use synchronization primitives like Critical Sections, Mutexes, etc, to protect against concurrency issues including Race Conditions, whenever you are accessing any data fields in your class (data module) or ANY globals. If you use these improperly you add deadlocks to your list of problems. So this is not a good place to mess up.
  • If you must access a VCL component or object in any way, do so indirectly via PostMessage, TThread.Synchronize, or some other thread-safe equivalent way of signaling the main thread that you need something done.
    • Think about what happens when you're shutting down. Maybe you could check if your data module even exists, since it might have gone away, before you invoke its methods.
Warren P
A: 

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.

BennyBechDk
A: 

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.

Massimo
I have edited your answer to make it more readable, please use the formatting button (or indent your code by 4 spaces) when posting source code. But better than adding an answer would be to edit the most important points into your question. I understand you want to access client data sets on the data module from worker threads. Is that right?
mghie
Is this threadsafe? It depends <g>. The code you've shown seems to be thread safe on first inspection. You didn't show what GetData() does. That might or might not be threadsafe. From what you've shown, when GetData returns, you assign 2 to the thread's task and have a thread using 100% cpu until the user closes the form.
Lieven
A: 

yes, mghie, it's right. Thanks for adjusting my code. Bye.

Massimo