2

I'm back with another question concerning threads and synchronization. Imagine a server application that has to perform a lengthy operation and the client wants his GUI to remain responsive while he waits for the server's response. I thought of the following pattern:

TMonitor.Enter (FTCPClient);
try
  WorkerThread := TWorkerThread.Create (SomeLengthyServerOperation);
  while (not WorkerThread.Ready) do
    Application.ProcessMessages;
  DoSometingWithResults (WorkerThread.Result);
  WorkerThread.Free;      
finally
  TMonitor.Exit (FTCPClient);
end;

WorkerThread is a simple class derived from TThread that executes the function passed to its constructor and then terminates (with Ready=True and the result in Result). The presented code is executed whenever a button is clicked.

Now to my question: If I click the button twice very fast, I get some strange errors that look a lot like the communication beetween server and client is somehow mengled, which I wanted to avoid by locking the FTCPClient object. In what thread are the event handlers after Application.ProcessMessages executed? Is the lock of TMonitor per thread? Does this mean that the lock does not work if I use Application.ProcessMessages?

I can't explain it better at the moment. I hope someone gets my point. If not, feel free to ask questions.

EDIT: For the Disabling and Enabling of the button: I don't know anything about the client code. Could be a button event handler, could be something else. Basically I want to hide the locking from the client code.

mjn
  • 36,362
  • 28
  • 176
  • 378
jpfollenius
  • 16,456
  • 10
  • 90
  • 156
  • Re your edit: It is still unclear why you would need to lock at all. The lock has nothing to do with the worker thread, and it has no effect on the main GUI thread. So why do you think you need it? – mghie Feb 20 '09 at 16:14
  • Because otherwise the messages get easily out of order (Request 1 -> Request 2 -> Success 1 -> Success 2 -> Result 1 -> Result 2 instead of Request 1 -> Success 1 -> Result 1 -> Request 2 -> Success 2 -> Result 2) – jpfollenius Feb 20 '09 at 19:43
  • AFAICS this has nothing to do with locking or not, and everything with calling Application.ProcessMessages without preventing recursive calls of the communication methods. Allen has explained this in his answer. – mghie Feb 20 '09 at 21:33
  • 1
    In the case anybody is interested: Putting the lock into the created worker thread did the job for me. – jpfollenius Feb 27 '09 at 13:49
  • See also my question and the included producer/consumer example (with the same workflow) using TMonitor for guarded blocks at http://stackoverflow.com/questions/3377579/what-is-tmonitor-in-delphi-system-unit-good-for – mjn Nov 21 '10 at 09:47

2 Answers2

8

TMonitor only blocks a different thread from acquiring the lock. What's happening is this: by processing messages from within the lock, you're getting back into the same function in the same thread, which is causing a recursive acquisition of the lock. Your code is then creating a new worker thread, and starting the cycle all over. You could disable the button so that you cannot click it again until the worker thread completes. Be sure you disable the button before you start processing the messages and use another try..finally block to ensure it gets re-enabled. Depending on how the rest of your code is arranged, you may not even need the lock.

chuacw
  • 1,685
  • 1
  • 23
  • 35
Allen Bauer
  • 16,657
  • 2
  • 56
  • 74
  • +1 Thanks for pointing that out. Please note my edit to the question concerning the disabling and enabling of the button. – jpfollenius Feb 20 '09 at 16:11
3

Some comments:

  1. Your WorkerThread sounds like you just reimplemented AsyncCalls. Using a tried and tested implementation is probably better than writing your own (unless you do it for the learning effect or because you have very special requirements). Please have a look at both AsyncCalls and the OmniThreadLibrary.

  2. Locking should happen only as short as possible, so wrapping your whole reaction to a button click in Monitor.Enter() and Monitor.Exit() seems wrong. It is also not really understandable what purpose this serves.

  3. Calling Application.ProcessMessages() while a lock is held can introduce all kinds of nasty surprises. If you need to keep your code from being reentered it is generally best to disable all the UI elements as the first thing a OnClick handler does, and reenable them when the handler is finished. Do also note that locks can be entered multiple times from the same thread, they only achieve exclusive access from multiple threads.

  4. All of the VCL executes in the main GUI thread, so locking is only ever necessary when you call the same code from background threads.

  5. If you have a look at this code you will see that you could achieve the same thing by doing your work in the GUI thread instead of spawning a worker thread.

I have posted this link several times on StackOverflow already, but please consider following the advice in this list posting for some things to keep in mind when doing multi-threaded programming. It has some good advice especially regarding the fifth point.

Edit: It's hard to say exactly, but your code should probably be something like this:

procedure TForm1.ActionStartExecute(Sender: TObject);
begin
  ActionStart.Enabled := FALSE;
  fWorkerThread := TWorkerThread.Create (Handle, SomeLengthyServerOperation);
end;

procedure TForm1.ActionStartUpdate(Sender: TObject);
begin
  ActionStart.Enabled := fWorkerThread = nil;
end;

procedure TForm1.WMThreadFinished(var AMsg: TWMThreadFinishedMsg);
begin
  // process results
  fWorkerThread := nil;
end;

TWorkerThread frees itself, but as the last action it posts a message to the form (that's why it gets the window handle as a parameter). In the handler for this message you set fWorkerThread to nil, so that the action is re-enabled in the next idle loop. Using a TAction means that you need not care what UI element originated the thread creation - they will all be disabled when the thread is created, and will be re-enabled when the thread has finished. No locking is necessary, and no new thread can be created while a thread is active.

mghie
  • 32,028
  • 6
  • 87
  • 129
  • Thanks! Please note that I made up the above example for illustration, so forget point 2. I can't use any external libraries in my case, that's why I did it myself. I will consider 3 and 4! And thanks for the link. – jpfollenius Feb 20 '09 at 15:58
  • Well, AsyncCalls is a single unit that you simply add to your project. – mghie Feb 20 '09 at 16:04
  • Okay, I will have a look at it! – jpfollenius Feb 20 '09 at 16:08
  • And OmniThreadLibrary is not a library (in the DLL sense) - it is a set of units that will compile into your program. – gabr Feb 20 '09 at 16:18
  • @gabr: Of course, and I didn't mean to imply it wasn't. Still, a single unit might be an easier sell ;-) IMHO the two libraries do also cater for completely different use cases, so everyone would have to decide for themselves. – mghie Feb 20 '09 at 16:42
  • @mghie: It looked to me that Smasher thinks that the OTL is some external entity - hence the explanation – gabr Feb 20 '09 at 17:32