1

I've been working with some multi-threaded applications, and part of this requires thread-protecting objects. I have individual object thread protection down by using the following method:

type
  TMyClass = class(TObject)
  private
    FLock: TRTLCriticalSection;
    FSomeString: String;
    procedure Lock;
    procedure Unlock;
    function GetSomeString: String;
    procedure SetSomeString(Value: String);
  public
    constructor Create;
    destructor Destroy; override;
    property SomeString: String read GetSomeString write SetSomeString;
  end;

implementation

constructor TMyClass.Create;
begin
  InitializeCriticalSection(FLock);
  Lock;
  try
    //Initialize some stuff
  finally
    Unlock;
  end;
end;

destructor TMyClass.Destroy;
begin
  Lock;
  try
    //Finalize some stuff
  finally
    Unlock;
  end;
  DeleteCriticalSection(FLock);
  inherited Destroy;
end;

procedure TMyClass.Lock;
begin
  EnterCriticalSection(FLock);
end;

procedure TMyClass.Unlock;
begin
  LeaveCriticalSection(FLock);
end;

function TMyClass.GetSomeString: String;
begin
  Result:= '';
  Lock;
  try
    Result:= FSomeString;
  finally
    Unlock;
  end;
end;

procedure TMyClass.SetSomeString(Value: String);
begin
  Lock;
  try
    FSomeString:= Value;
  finally
    Unlock;
  end;
end;

However, when I implement a list of objects, I can't figure out how to safely protect each object. I create my object lists like this:

type
  TMyClass = class;
  TMyClasses = class;

  TMyClass = class(TObject)
  private
    FOwner: TMyClasses;
  public
    constructor Create(AOwner: TMyClasses);
    destructor Destroy; override;
  end;

  TMyClasses = class(TObject)
  private
    FItems: TList;
    function GetMyItem(Index: Integer): TMyItem;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Clear;
    function Count: Integer;
    property Items[Index: Integer]: TMyClass read GetMyItem; default;
  end;

implementation

{ TMyClass }

constructor TMyClass.Create(AOwner: TMyClasses);
begin
  FOwner:= AOwner;
  FOwner.FItems.Add(Self);
  //Initialize some stuff...
end;

destructor TMyClass.Destroy;
begin
  //Uninitialize some stuff...
  inherited Destroy;
end;

{ TMyClasses }

constructor TMyClasses.Create;
begin
  FItems:= TList.Create;
end;

destructor TMyClasses.Free;
begin
  Clear;
  FItems.Free;
  inherited Destroy;
end;

procedure TMyClasses.Clear;
begin
  while FItems.Count > 0 do begin
    TMyClass(FItems[0]).Free;
    FItems.Delete(0);
  end;
end;

function TMyClasses.Count: Integer;
begin
  Result:= FItems.Count;
end;

function TMyClasses.GetMyItem(Index: Integer): TMyClass;
begin
  Result:= TMyClass(FItems[Index]);
end;

There are two ways I see doing this, and both ways I don't trust. One way would be to implement a critical section lock in the list object (TMyClasses) and each object within would share this lock (by calling FOwner.Lock; and FOwner.Unlock;. But then two different threads wouldn't even be able to work with two different objects from this list at one time, and would defeat the purpose of multithreading. The second way would be to put another critical section in each individual object of their own, but too many of these is also dangerous, right? How can I protect the list and every object in the list together?

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
Jerry Dodge
  • 26,858
  • 31
  • 155
  • 327
  • 1
    Take a look at [`TThreadList`](http://docwiki.embarcadero.com/Libraries/XE3/en/System.Generics.Collections.TThreadList). Here's an [`example`](http://docwiki.embarcadero.com/CodeExamples/XE3/en/TThreadList_(Delphi)) for non-generic version of it. – TLama Nov 26 '12 at 20:51
  • @TLama - True, I am familiar, but this requires that the end-user be responsible to implement the locks from the outside. I'm trying to eliminate this need to even worry about lock/unlock, and handle it all from within this object structure. Plus, suppose you acquire a pointer to an object within this list, then you try to do something to this object its self well after the list has been "unlocked". Not good. – Jerry Dodge Nov 26 '12 at 20:51
  • 2
    @JerryDodge "Plus, suppose you acquire a pointer to an object within this list, then you try to do something to this object its self well after the list has been "unlocked". Not good" You have to decide on the rules and ask all parties to stick to them. Nothing wrong with that. What you are criticising is exactly what you did in `TMyClass.Lock`. – David Heffernan Nov 26 '12 at 20:58
  • 2
    What you are attempting to do with your list is impossible. You can't ask for `Count`, and then subsequently ask for `Items[i]`, without a lock being held the entire time. – David Heffernan Nov 26 '12 at 21:07
  • Apologies, that first example was wrong and I just corrected it. I was merely demonstrating that I knew how to implement a lock, but failed to clarify exactly how I use it. – Jerry Dodge Nov 26 '12 at 21:07
  • @DavidHeffernan When you put it that way, it kinda makes sense now... So in this case, is it only possible using a thread list as TLama pointed out? – Jerry Dodge Nov 26 '12 at 21:11
  • @Jerry That's a perfectly good way to do it. – David Heffernan Nov 26 '12 at 21:18
  • 1
    You don't need to call Lock/Unlock in `TMyClass` constructor or destructor. A constructor only runs once, and if you call a destructor twice then it's all over no matter what. – David Heffernan Nov 26 '12 at 21:29
  • Thanks David for fixing that, I need to slow down and pay a little closer attention. – Jerry Dodge Nov 26 '12 at 21:35
  • You don't need a CriticalSection in each instance of your object. Use [TMonitor.Enter](http://docwiki.embarcadero.com/Libraries/XE2/en/System.TMonitor.Enter)/Exit/TryEnter for that! – jachguate Nov 26 '12 at 21:40
  • 2
    @jachguate Did you miss the part where `TMonitor` doesn't work? Is it fixed now? – David Heffernan Nov 26 '12 at 21:41
  • @David maybe I missed it, do you have a link? – jachguate Nov 26 '12 at 21:42
  • @jachguate http://www.google.com/search?q=delphi+tmonitor+bug – David Heffernan Nov 26 '12 at 21:45
  • @JerryDodge What do you expect from that list? Having some threads working on one item or on all items in one go? – Sir Rufo Nov 26 '12 at 22:13
  • 1
    @David I now remember about this [bug exposed here in SO](http://stackoverflow.com/questions/4856306), but it was resolved since XE2 Update 4. I'm successfully running the posted example and variations in XE3. Are you aware of another bug in TMonitor? I'm not being lazy (activelly searching in google since my first comment), just asking for a comment if you're or I should stop searching. – jachguate Nov 26 '12 at 22:22
  • @jachguate I pretty much lost faith in `TMonitor` over that bug. I've no evidence that there are more problems with it. Personally for a mutex type lock, I prefer to use system native locks, e.g. Windows critical section. I trust them to work. – David Heffernan Nov 26 '12 at 22:25
  • @David I won't blame you about that! What I like about TMonitor is it's supposed to be lightweight and faster than a OS based lock. – jachguate Nov 26 '12 at 22:36
  • @jachguate I'd be surprised if it was faster than a critical section. Now it's your turn to provide some links! Do you have links to discussion of `TMonitor` performance? – David Heffernan Nov 26 '12 at 22:37
  • @David No links for now, I'll make some tests and I'll let you know my results. :) – jachguate Nov 26 '12 at 22:41
  • 4
    Seems as the threading model is a bit unclear. Best is to model it as a producer/consumer case where one (or many) producers put the objects into one single queue (TThreadedQueue) and let one or many consumers work on the objects from the queue. When work is done they can put the objects into a result queue (also a TThreadedQueue),where the producer(s) can decide what to do. – LU RD Nov 26 '12 at 22:56
  • 1
    @David my tests show that it depends on usage, calling a single Critical Section Enter/Leave versus TMonitor.Enter/Exit on a single Object, the Critical Section performs 2x faster than monitor. Creating thousands of Critical Sections and Enter/Leave versus calling TMonitor Enter/Exit over thousands of different objects, TMonitor performs 2x faster than Critical Sections. Real world is like a mixture of both in most applications. – jachguate Nov 27 '12 at 00:04
  • 1
    Additionally I found [this blog](http://delphihaven.wordpress.com/2011/05/30/the-tyranny-of-simple-tests/) post from Chris Rolliston talking about performance of Monitor vs CriticalSection – jachguate Nov 27 '12 at 00:05
  • @LURD I have not mentioned any type of queue. It's just a simple list of objects. – Jerry Dodge Nov 27 '12 at 02:10
  • 1
    @JerryDodge, the queue is there just to delegate the objects. You mention that these objects has to be protected by individual threads. So instead of keeping them inside of the list, pop them into a queue so the threads can do some work. Later when the threads are ready and has put the objects into a result queue, you take it back into your list. The whole purpose here is to keep the workflow clear and let the different parts do simple tasks on a need to know basis. – LU RD Nov 27 '12 at 06:53
  • @JerryDodge, and if the keeper of the list doesn't have a clue how to delegate, skip the queues and let the threads pop an object of your threadlist and when done push it back into the list again. So when work is being done, the object(s) are removed from the threadlist. – LU RD Nov 27 '12 at 07:32

1 Answers1

1

You cannot realistically expect to use the same approach in your list class as you use in the simple class that serializes access to a single object.

For example, your list class has, like so many before it, a Count property, and an indexed Items[] property. I'm going to presume that your threading model allows the list to mutate. Now, suppose you want to write code like this:

for i := 0 to List.Count-1 do
  List[i].Frob;

Suppose that another thread were to mutate the list whilst this loop was running. Well, that would clearly lead to runtime failures. So, we can conclude that the loop above would need to be wrapped with a lock. Which means that thread-safety aspects of the list must be exposed externally. You cannot keep it all internal with the current design.

If you wish to keep the lock internal to the class you'll have to remove the Count and Items[] properties. You could have your list looking like this (with some parts removed):

type
  TThreadsafeList<T> = class
  private
    FList: TList<T>;
    procedure Lock;
    procedure Unlock
  public
    procedure Walk(const Visit: TProc<T>);
  end;
....
procedure TThreadsafeList<T>.Walk(const Visit: TProc<T>);
var
  Item: T;
begin
  Lock;
  try
    for Item in FList do
      Visit(Item);
  finally
    Unlock;
  end;
end;

And now you can replace the loop above with this:

ThreadsafeList.Walk(
  procedure(Item: TMyItemClass)
  begin
    Item.Frob;
  end
);

It's not difficult to extend this concept to allow for your Walk method to support deletion of certain items, as determined by the Visit procedure.

But as you say, quite what you can do with such a list is moot. Shared data is the bane of multi-threading. I suggest you find a way to solve your problem that gives each thread its own private copy of all data that it needs. At which point you need no synchronisation and it's all good.

One final point. There is no single concept of thread safety. What is meant by thread safety varies from context to context. Eric Lippert said it best: What is this thing you call "thread safe"? So anytime you ask a question like this, you should give plenty of detail on your particular use case and threading model.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • 1
    This protects only the List and not the contained Items. OP asked for List and Item protection – Sir Rufo Nov 26 '12 at 21:47
  • @SirRufo What do you mean? I don't understand. I assume that the items are owned by the list since that's how they seem to be in Jerry's list. And since they can only be accessed with `Walk` which is serialized, I can't see what your complaint is. – David Heffernan Nov 26 '12 at 21:54
  • Yes from that POV it is true, but the original question was: `But then two different threads wouldn't even be able to work with two different objects from this list at one time, and would defeat the purpose of multithreading` – Sir Rufo Nov 26 '12 at 21:56
  • 1
    @SirRufo It's hard to have it both ways. You could use multiple locks internal to the class and retain the same `Walk` interface. But that would be very complex to implement. For exactly these issues, multi-threading is best done without using shared mutable data. – David Heffernan Nov 26 '12 at 21:59
  • ;o) yes ... but i think (just a guess) OP is looking for a threadsafe queue to perform actions from multiple threads – Sir Rufo Nov 26 '12 at 22:04
  • 1
    @SirRufo Threadsafe queue is simple enough to implement efficiently with a single lock. – David Heffernan Nov 26 '12 at 22:07
  • Great solution, but as Sir Rufo points out, I'm trying to protect the actual objects inside the list, not just the list. – Jerry Dodge Nov 27 '12 at 02:11
  • @Jerry This class protects the objects as well. – David Heffernan Nov 27 '12 at 07:22
  • If you want to allow different threads to modify the contents of the list then you are led to solutions like this or TThreadList. I offered this option as an alternative to TThreadList. – David Heffernan Nov 27 '12 at 07:27
  • @Jerry A full qualified answer that you will accept is not possible without being a book about thread programming. As David points out `So anytime you ask a question like this, you should give plenty of detail on your particular use case and threading model.`. There are some basics for threading, but you have to combine them wise. It's like cooking - although you have all ingredients and tools it is not guaranteed to get a perfect meal – Sir Rufo Nov 27 '12 at 09:02