Solved

worker thread updating an object created in main thread

Posted on 2003-11-23
9
896 Views
Last Modified: 2013-11-23
I am having some problems with a simple main thread + single worker thread application .. they may be due to my logic or perhaps to a misunderstanding of thread communication. I cannot reduce the problem to a simple example as yet, so I am looking for conceptual feedback.

I have a worker thread .. TworkerThread=class(Tthread). I want this to automatically FreeOnTerminate and it is convenient for me to have the thread tell another object (TWatcher=class(Tcomponent) ) about what it is doing, where it (the thread) has got to, what the status is. The watcher is owned by the form and survives the thread.

So I instantiate the Watcher in the caller (the VCL) and then set a property of the thread (eg property MyWatcher:Twatcher) to point to it
before resuming the thread. In the Execute method, MyWatcher properties (eg MyWatcher.CasesProcessed) are updated.

MyWatcher is only freed by the form, long after the thread has died.


There is only ONE worker thread per Watcher. Only the WorkerThread can update the Watcher and the main (VCL) thread only ever queries it.

My assumption here is that because the WorkerThread and the MainThread share the same address space, that whenever the WorkerThread alters a property of the Watcher, the updated Watcher properties will be available in the MainThread (whenever that gets switched to).


This is the crux of the problem. Can I be sure that any object that is instantiated in the main thread and updated in a worker thread (whch knows the pointer to that object)  will be updated immediately? And hence will be accessible in its updated state to the worker thread and the main thread?

My architecture is, I think, simple. A MainThread, a Watcher instantiated in the MainThread and a pointer to that Watcher passed to the WorkerThread : the WorkerThread directly sets and gets properties of the Watcher, the MainThread examines them as needed.

looking for some elucidation here .. is my mental model wrong? can a thread update properties of an object it points to without issues of time delay and thread context switching?

I need conceptual help, and soon.
0
Comment
Question by:Mutley2003
  • 3
  • 2
  • 2
  • +2
9 Comments
 
LVL 8

Expert Comment

by:gmayo
Comment Utility
One thing you have to watch for is corruption of data caused by two threads trying to update the same data at once. For this you should use critical sections, mutexes, or other forms of protection. There is no reason why you can't do what you've said, but just remember the issues.

Geoff M.
0
 
LVL 17

Assisted Solution

by:geobul
geobul earned 100 total points
Comment Utility
Hi,

Look at this simple example using a label on a form in a thread. The example uses Synchronize method. As Geoff said you could use messages, critical section, etc. instead.

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TForm1 = class(TForm)
    btnCreateThread: TButton;
    btnTerminateThread: TButton;
    Label1: TLabel;
    btnShowLabel: TButton;
    procedure btnTerminateThreadClick(Sender: TObject);
    procedure btnCreateThreadClick(Sender: TObject);
    procedure btnShowLabelClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

  TTransactionThread = class(TThread)
    MyLabel: TLabel;
    procedure Execute; override;
    procedure Oops;
  private
    c: integer;
  end;

var
  Form1: TForm1;
  TransactionThread: TTransactionThread;

implementation

{$R *.DFM}

// Thread code

procedure TTransactionThread.Oops;
begin
  MyLabel.Caption := 'Pass '+IntToStr(c);
end;

procedure TTransactionThread.Execute;
var
  k: integer;
begin
 k := 0;
 c := 0;
 while (not terminated) and (k <> 5) do begin
   sleep(1000);
   inc(k);
   if k = 5 then begin
     // the actual code goes here (loop with delete)
     inc(c);
     Synchronize(Oops);
     k := 0;
   end;
 end;
end;

// Form code

procedure TForm1.btnTerminateThreadClick(Sender: TObject);
begin
  TransactionThread.Terminate;
end;

procedure TForm1.btnCreateThreadClick(Sender: TObject);
begin
  TransactionThread := TTransactionThread.Create(true);
  TransactionThread.FreeOnTerminate := true;
  TransactionThread.MyLabel := Form1.Label1;
  TransactionThread.Resume;
end;

procedure TForm1.btnShowLabelClick(Sender: TObject);
begin
  ShowMessage(Label1.Caption);
end;

initialization

finalization
  TransactionThread.Terminate;

end.

Regards, Geo
0
 
LVL 17

Assisted Solution

by:Wim ten Brink
Wim ten Brink earned 100 total points
Comment Utility
You still need to synchronize with the watcher thread if the worker wants to update watcher properties, especially when modifying string properties, pointers or child objects. Delphi's synchronization method like Geo points out should work nicely, except for one issue. Geo is synchronizing with the main thread and you actually need to synchronize with the Watcher thread. So you might need some inter-thread communications instead. Probably means using mutexes, events or semaphores, possibly combined with sending messages between threads.
I had to solve a similar situation once for a scheduler service. I needed one thread who was just using for user input, a second thread keeping an eye on the clock ans starting applications at certain times and a third thread that would start and stop executables. The whole combination had to be as stable as possible, was not allowed to eat away too much resources and should be able to run unsupervised for weeks, even months. I managed to do this but it involved adding a lot of synchronisizing between all the threads that were running. WaitForMultipleObject combined with a short timeout is quite useful too in these cases. So are critical sections. Use them or risk data corruption...

And yes, even a boolean value can be corrupted. I discovered that the hard way since I used a boolean property in my project. What happened was that the set method was called and halfway the execution, the other thread would be activated, call the get method, notice it wasn't set while it was actually about to be set and my system would crash because it was looking for resources that were freed when I tried to set that boolean property...
The only types of data that you can reasonable safely access in multi-threaded applications are single-byte variables. Even a 2-byte variable can be corrupted because a thread could stop while only one of the two bytes has been changed, and the other is still unchanged. Leads to many, nasty bugs.
0
 
LVL 17

Expert Comment

by:geobul
Comment Utility
Hi Alex,

Will there be a different thread for the Watcher? It is a component on a form, i.e. part of the VCL thread, isn't it? The worker thread needs to change a property of that component, so Synchronize with the VCL should be enough, I think.

Regards, Geo
0
How to run any project with ease

Manage projects of all sizes how you want. Great for personal to-do lists, project milestones, team priorities and launch plans.
- Combine task lists, docs, spreadsheets, and chat in one
- View and edit from mobile/offline
- Cut down on emails

 

Author Comment

by:Mutley2003
Comment Utility

ALEX said
....

The only types of data that you can reasonable safely access in multi-threaded applications are single-byte variables. Even a 2-byte variable can be corrupted because a thread could stop while only one of the two bytes has been changed, and the other is still unchanged. Leads to many, nasty bugs


The SDK says
<SDK>
PlatformSDK:DLLs, Processes, and Threads

Interlocked Variable Access
The interlocked functions provide a simple mechanism for
synchronizing access to a variable that is shared by multiple
threads. The threads of different processes can use this
mechanism if the variable is in shared memory.


Simple reads and writes to properly-aligned 32-bit variables are
atomic. In other words, when one thread is updating a 32-bit
variable, you will not end up with only one portion of the
variable updated; all 32 bits are updated in an atomic fashion.
However, access is not guaranteed to be synchronized. If two
threads are reading and writing from the same variable, you
cannot determine if one thread will perform its read operation
before the other performs its write operation.
...
</SDK>

So,
Even if the variables are "properly aligned" (and I am not sure that
Delphi DOES align fields in an object on 32 bit boundaries ... anyone?), I read this as saying that you STILL cannot trust updating
even a simple global integer variable from the VCL and a worker thread, or even more simply, updating it in the worker thread and reading it in the VCL thread (which is what I wanted to do). This is because there is no guarantee of synchronized access.

OK, do people agree with me that this is unsafe with a simple global integer?

Then how about a property that has just simple fields for access (ie no getters and setters) .
like
property Status : integer read fstatus write fstatus

Is this any more unsafe? (as in attempting to write it from a worker thread and "subsequently" read it from the VCL thread

Alex's example of a setter on a Boolean property being corrupted I can now understand given that I have started to think about context switching happening 18 times per second.

But would the following code fix it?

procedure TtransactionThread.TurnMyBooleanOff;
begin
form1.MyBoolean := false;
end;

procedure TtransactionThread.TurnMyBooleanOn;
begin
form1.MyBoolean := true;
end;

procedure TtransactionThread.SetMyBoolean(const Val:Boolean);
begin
if val then synchronize(TurnMyBooleanOn) else
Synchronize(TurnMyBooleanOff);
end;

It looks like it should work, but it sure is ugly (the procedure called by Synchronize cannot have any arguments).

Now, what if I want to READ form1.MyBoolean in the TransactionThread?

I could introduce a new field in TTransactionThread as
private __MyBoolean : Boolean

and another 3 methods

procedure TTransactionThread.__getMyBoolean;
begin
__MyBoolean :=form1.MyBoolean;
end;

procedure TTransactionThread.getMyBoolean;
begin
synchronize(__getMyBoolean);
end;

function TTransactionThread.MyBoolean :Boolean;
begin
getMyBoolean;
result := __MyBoolean;
end;

which is truly ugly. I am not even sure it will work... it seems as if you are updating a thread local field __MyBoolean by running in the VCL thread.

Is synchronize just a 1 way street then ?



Geo,
I noticed you put in

finalization
  TransactionThread.Terminate;

but with FreeOnTerminate :=True, surely TransactionThread might be
already freed at this time?

This leads me to the issue of managing thread lifetimes, one of the reasons I built my Watcher component (so I could query it about the thread even though the thread might have been freed).

What is the consensus about using FreeOnTerminate?

I have been setting it as True mostly because of lack of a sensible place to do the freeing. in Finalization is one possibility, so I guess is in the OnTerminate event handler - but the problem from my point of view is that the code  looks disjointed .. you start the thread in one procedure and do the cleanup and access to results somewhere else.

thanks for the help, people.






 






0
 

Author Comment

by:Mutley2003
Comment Utility
I have looked a bit further at this and there is an interesting discussion
at
http://www.experts-exchange.com/Programming/Programming_Languages/Cplusplus/Q_20416964.html
re what is and is not atomic (if atomic == 1 machine isntruction and cannot be interrupted by task switching).

The consensus appears to be that simple 32 bit assignment is atomic, but not much else. But the issue was also raised that even though the operation is atomic the result might be stored in a processor cache and maybe that would have an effect (possibly an effect on synchronization). There is a C++ keyword volatile to force stuff straight to memory, but I don't know the Delphi equivalent.

The suggestion was made to always use mutexes when accessing shared data. I thought the idea of mutexes was like critical sections when WRITING data .. I am still unclear as to how mutexes might be used in Delphi to ensure synchronization.
0
 
LVL 8

Assisted Solution

by:gmayo
gmayo earned 100 total points
Comment Utility
I think you may need to use critical sections if you want to pass parameters, which Synchronize doesn't allow. So, your program could look something like this:

TMyClass = class
private
  fMyString : string;
  function GetMyString : string;
  procedure SetMyString(s : string);
public
  property MyString : string read GetMyString write SetMyString;
end;

function TMyClass.GetMyString : string;
begin
  MyLock.Acquire;
  Result := fMyString;
  MyLock.Release;
end;

procedure TMyClass.SetMyString(s : string);
begin
  MyLock.Acquire;
  fMyString := s;
  MyLock.Release;
end;

Now you can read and write MyClass.MyString without worrying about corruption. Note that you need to create and destroy the MyLock (critical section) variable.

Geoff M.
0
 
LVL 33

Accepted Solution

by:
Slick812 earned 200 total points
Comment Utility
hello Mutley2003, For me I would always try and use some sort of thread syncro just to be sure, althouh if you are only doing a single 32 bit value assignment you might can get by without it. I consider a Mutex if there is More than one process to access some shared memory. . . For a single process (what you have described) There is a very handy Delphi utility called TMultiReadExclusiveWriteSynchronizerin SysUtils, for muti thread access control, the delphi help says -

Use TMultiReadExclusiveWriteSynchronizer to guard access to memory in a multi-threaded application.
Unlike a critical section, which blocks all other threads from reading or writing its associated memory,
TMultiReadExclusiveWriteSynchronizer allows multiple threads to read from the protected memory simultaneously,
while ensuring that any thread writing to the memory has exclusive access.

you can set up a variable for your TMultiReadExclusiveWriteSynchronizer for each Thread so each thread only accesses a single TMultiReadExclusiveWriteSynchronizer. . . .

var
FLock: TMultiReadExclusiveWriteSynchronizer;

and somewhere in yout code before you need to use it create the TMultiReadExclusiveWriteSynchronizer object . . . .

FLock := TMultiReadExclusiveWriteSynchronizer.Create;

you will need to Free it also, since it does not have a owner;

now before you READ or WRITE to any Thread-Shared variable, call FLock.BeginWrite; or FLock.BeginRead; and then call FLock.EndWrite; or FLock.EndRead; , , , probally in a try and finally block


FLock.BeginWrite;
try
  Thread-SharedObj.Value := 55;
  Thread-SharedVar := NewValue;
  finally
  FLock.EndWrite;
  end;

 - - - - - - - - - - - - - -  - - - -

FLock.BeginRead;
try
  aValue := Thread-SharedObj.Value;
  NewValue := Thread-SharedVar;
  finally
  FLock.EndRead;
  end;

- - - - - - - - - - - - - - - - - - - - -  -
that should do it for ya
the help also says

In applications where threads read from an object or variable frequently and only write to it occasionally,
using the multi-read exclusive-write synchronizer instead of a critical section can result in considerable performance improvement.

All access to the protected memory must be bracketed by calls to the BeginRead and EndRead or
BeginWrite and EndWrite methods. Any thread that reads from or writes to this memory without using
these calls can introduce thread conflicts.

This seems like a good way to do what you want and ensure acurrate reads with thread safety
0
 

Author Comment

by:Mutley2003
Comment Utility
I am going to close this off because I have had enough help from you people to get me to the next stage.. I needed some education and some pointers, I got that, I think I can use critical sections to do what I need, and most importantly I feel much more comfortable and in control of what I am doing. I'll post again when I get to the next stage of difficulty <g>. So long, and thanks for all the fish.
0

Featured Post

Highfive + Dolby Voice = No More Audio Complaints!

Poor audio quality is one of the top reasons people don’t use video conferencing. Get the crispest, clearest audio powered by Dolby Voice in every meeting. Highfive and Dolby Voice deliver the best video conferencing and audio experience for every meeting and every room.

Join & Write a Comment

Hello everybody This Article will show you how to validate number with TEdit control, What's the TEdit control? TEdit is a standard Windows edit control on a form, it allows to user to write, read and copy/paste single line of text. Usua…
Introduction I have seen many questions in this Delphi topic area where queries in threads are needed or suggested. I know bumped into a similar need. This article will address some of the concepts when dealing with a multithreaded delphi database…
This video discusses moving either the default database or any database to a new volume.
This video explains how to create simple products associated to Magento configurable product and offers fast way of their generation with Store Manager for Magento tool.

762 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

10 Experts available now in Live!

Get 1:1 Help Now