What is TMonitor in Delphi System unit good for?
Asked Answered
M

1

34

After reading the articles "Simmering Unicode, bring DPL to a boil" and "Simmering Unicode, bring DPL to a boil (Part 2)" of "The Oracle at Delphi" (Allen Bauer), Oracle is all I understand :)

The article mentions Delphi Parallel Library (DPL), lock free data structures, mutual exclusion locks and condition variables (this Wikipedia article forwards to 'Monitor (synchronization)', and then introduces the new TMonitor record type for thread synchronization and describes some of its methods.

Are there introduction articles with examples which show when and how this Delphi record type can be used? There is some documentation online.

  • What is the main difference between TCriticalSection and TMonitor?

  • What can I do with the Pulse and PulseAllmethods?

  • Does it have a counterpart for example in C# or the Java language?

  • Is there any code in the RTL or the VCL which uses this type (so it could serve as an example)?


Update: the article Why Has the Size of TObject Doubled In Delphi 2009? explains that every object in Delphi now can be locked using a TMonitor record, at the price of four extra bytes per instance.

It looks like TMonitor is implemented similar to Intrinsic Locks in the Java language:

Every object has an intrinsic lock associated with it. By convention, a thread that needs exclusive and consistent access to an object's fields has to acquire the object's intrinsic lock before accessing them, and then release the intrinsic lock when it's done with them.

Wait, Pulse and PulseAll in Delphi seem to be counterparts of wait(), notify() and notifyAll() in the Java programming language. Correct me if I am wrong :)


Update 2: Example code for a Producer/Consumer application using TMonitor.Wait and TMonitor.PulseAll, based on an article about guarded methods in the Java(tm) tutorials (comments are welcome):

This kind of application shares data between two threads: the producer, that creates the data, and the consumer, that does something with it. The two threads communicate using a shared object. Coordination is essential: the consumer thread must not attempt to retrieve the data before the producer thread has delivered it, and the producer thread must not attempt to deliver new data if the consumer hasn't retrieved the old data.

In this example, the data is a series of text messages, which are shared through an object of type Drop:

program TMonitorTest;

// based on example code at http://download.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

{$APPTYPE CONSOLE}

uses
  SysUtils, Classes;

type
  Drop = class(TObject)
  private
    // Message sent from producer to consumer.
    Msg: string;
    // True if consumer should wait for producer to send message, false
    // if producer should wait for consumer to retrieve message.
    Empty: Boolean;
  public
    constructor Create;
    function Take: string;
    procedure Put(AMessage: string);
  end;

  Producer = class(TThread)
  private
    FDrop: Drop;
  public
    constructor Create(ADrop: Drop);
    procedure Execute; override;
  end;

  Consumer = class(TThread)
  private
    FDrop: Drop;
  public
    constructor Create(ADrop: Drop);
    procedure Execute; override;
  end;

{ Drop }

constructor Drop.Create;
begin
  Empty := True;
end;

function Drop.Take: string;
begin
  TMonitor.Enter(Self);
  try
    // Wait until message is available.
    while Empty do
    begin
      TMonitor.Wait(Self, INFINITE);
    end;
    // Toggle status.
    Empty := True;
    // Notify producer that status has changed.
    TMonitor.PulseAll(Self);
    Result := Msg;
  finally
    TMonitor.Exit(Self);
  end;
end;

procedure Drop.Put(AMessage: string);
begin
  TMonitor.Enter(Self);
  try
    // Wait until message has been retrieved.
    while not Empty do
    begin
      TMonitor.Wait(Self, INFINITE);
    end;
    // Toggle status.
    Empty := False;
    // Store message.
    Msg := AMessage;
    // Notify consumer that status has changed.
    TMonitor.PulseAll(Self);
  finally
    TMonitor.Exit(Self);
  end;
end;

{ Producer }

constructor Producer.Create(ADrop: Drop);
begin
  FDrop := ADrop;
  inherited Create(False);
end;

procedure Producer.Execute;
var
  Msgs: array of string;
  I: Integer;
begin
  SetLength(Msgs, 4);
  Msgs[0] := 'Mares eat oats';
  Msgs[1] := 'Does eat oats';
  Msgs[2] := 'Little lambs eat ivy';
  Msgs[3] := 'A kid will eat ivy too';
  for I := 0 to Length(Msgs) - 1 do
  begin
    FDrop.Put(Msgs[I]);
    Sleep(Random(5000));
  end;
  FDrop.Put('DONE');
end;

{ Consumer }

constructor Consumer.Create(ADrop: Drop);
begin
  FDrop := ADrop;
  inherited Create(False);
end;

procedure Consumer.Execute;
var
  Msg: string;
begin
  repeat
    Msg := FDrop.Take;
    WriteLn('Received: ' + Msg);
    Sleep(Random(5000));
  until Msg = 'DONE';
end;

var
  ADrop: Drop;
begin
  Randomize;
  ADrop := Drop.Create;
  Producer.Create(ADrop);
  Consumer.Create(ADrop);
  ReadLn;
end.

Now this works as expected, however there is a detail which I could improve: instead of locking the whole Drop instance with TMonitor.Enter(Self);, I could choose a fine-grained locking approach, with a (private) "FLock" field, using it only in the Put and Take methods by TMonitor.Enter(FLock);.

If I compare the code with the Java version, I also notice that there is no InterruptedException in Delphi which can be used to cancel a call of Sleep.

Update 3: in May 2011, a blog entry about the OmniThreadLibrary presented a possible bug in the TMonitor implementation. It seems to be related to an entry in Quality Central. The comments mention a patch has been provided by a Delphi user, but it is not visible.

Update 4: A blog post in 2013 showed that while TMonitor is 'fair', its performance is worse than that of a critical section.

Morven answered 31/7, 2010 at 9:41 Comment(1)
TMonitor had severe bugs, which were finally corrected in XE2 upd 4. The errors could be manifested by the TMonitor use in TThreadedQueue. See TThreadedQueue not capable of multiple consumers? for more information.Mlawsky
R
13

TMonitor combines the notion of a critical section (or a simple mutex) along with a condition variable. You can read about what a "monitor" is here:

http://en.wikipedia.org/wiki/Monitor_%28synchronization%29

Any place you would use a critical section, you can use a monitor. Instead of declaring a TCriticalSection, you can simple create a TObject instance and then use that:

TMonitor.Enter(FLock);
try
  // protected code
finally
  TMonitor.Exit(FLock);
end;

Where FLock is any object instance. Normally, I just create a TObject:

FLock := TObject.Create;
Retain answered 31/7, 2010 at 16:42 Comment(8)
So this FLock is the monitor in this example? (the "object intended to be used safely by more than one thread" as described in Wikipedia)Morven
FLock is any object instance. It can be just a simple TObject instance. FLock := TObject.Create;Retain
Still not enough. You have shown how to emulate a critical section with TMonitor, but sure that is not the real problem the TMonitor was designed for. Can you give a more interesting code example?Airglow
You might also want to add some information why you chose to add to the VCL the generally reviled ability to lock any object. See for example https://mcmap.net/q/73617/-why-is-lock-this-bad.Widgeon
Mjustin, I wouldn't say FLock is the monitor in this example. It has a monitor, and we're using it to protect some code. If we already had some other TObject, we'd probably have used its monitor instead of creating a new object for that sole purpose. It isn't necessarily the object being used by multiple threads — that's whatever is used inside the try-finally section — but to have any benefit at all, the other threads will need to know about FLock so they can wait on the same object. (Just like a critical section, it's useless if only one thread uses it.)Mezzo
@Rob: Wikipedia defines Monitor as 'an object intended to be used safely by more than one thread' - in the example one locked object (FLock) is used to protect code in a different object. The article mghie links says that it is bad idea to create a global lock on the object ("Why is lock(this) {…} bad?"). Other threads do not need to 'know' (have access to) FLock, FLock can be a private instance variable, only used in one synchronized method.Morven
Other threads do need to know about FLock, or at least the object FLock refers to. That's not the same as saying other portions of code need to know about it. It could indeed be a private instance variable, but then other threads need to call methods on that instance. The object cannot be stored solely in a local variable; it needs to be accessible to multiple threads. I don't mean TThread objects, just ordinary OS threads of execution.Mezzo
I have added example code for a Producer/Consumer application, using TMonitor.Wait and TMonitor.PulseAll. It locks on the shared container object, but I added a note that it could also implement fine-grained locking.Morven

© 2022 - 2024 — McMap. All rights reserved.