1

I have simple thread safe container class. It has standard add/remove methods. Normally enumerating items is implemented as:

MyList.lock;
try
  // looping here
finally
  MyList.unlock;
end;

But I want to take advantage of for-in support in a thread safe manner:

for item in MyList do 
begin
  // do something
end;

My enumerator implementation locks the container in it's contructor and unlocks it in the destructor. This works but lies on the assumption that Enumerator's instance is created at the beginning of for-in loop and is destroyed at the end. I found that explanation here: How is Enumerator created with for in construction destroyed?

But since locking/unlocking is a critical operation I wonder if this kind of usage is ok?

Here is my implementation:

  TContainer<T> = class
    private
      FPadLock: TObject;
      FItems: TList<T>;
    protected
    public
      type
        TContainerEnumerator = class(TList<T>.TEnumerator)
          private
            FContainer: TContainer<T>;
          public
            constructor Create(AContainer: TContainer<T>);
            destructor Destroy; override;
        end;
      constructor Create;
      destructor Destroy; override;
      procedure add(AItem: T);
      procedure remove(AItem: T);
      function GetEnumerator: TContainerEnumerator;
  end;

{ TContainer<T> }

procedure TContainer<T>.add(AItem: T);
begin
  TMonitor.Enter(FPadLock);
  try
    FItems.Add(AItem);
  finally
    TMonitor.Exit(FPadLock);
  end;
end;

constructor TContainer<T>.Create;
begin
  inherited Create;
  FPadLock := TObject.Create;
  FItems := TList<T>.Create;
end;

destructor TContainer<T>.Destroy;
begin
  FreeAndNil(FItems);
  FreeAndNil(FPadLock);
  inherited;
end;

procedure TContainer<T>.remove(AItem: T);
begin
  TMonitor.Enter(FPadLock);
  try
    FItems.Remove(AItem);
  finally
    TMonitor.Exit(FPadLock);
  end;
end;

function TContainer<T>.GetEnumerator: TContainerEnumerator;
begin
  result := TContainerEnumerator.Create(self);
end;

{ TContainer<T>.TContainerEnumerator }

constructor TContainer<T>.TContainerEnumerator.Create(
  AContainer: TContainer<T>);
begin
  inherited Create(AContainer.FItems);
  FContainer := AContainer;
  TMonitor.Enter(FContainer.FPadLock);  // <<< Lock parent container using Monitor
end;

destructor TContainer<T>.TContainerEnumerator.Destroy;
begin
  TMonitor.Exit(FContainer.FPadLock);  // <<< Unlock parent container
  inherited;
end;
Community
  • 1
  • 1
iPath ツ
  • 2,468
  • 20
  • 31
  • If you care about perf, esp in multi threaded env, you won't allocate enumerators on the heap – David Heffernan Oct 23 '14 at 09:42
  • @DavidHeffernan: I only want to be sure that the initial Lock will be followed by matching Unlock after exiting for-in loop. – iPath ツ Oct 23 '14 at 09:45
  • @DavidHeffernan: for example, if an exception occurs within for-in's body I need to be sure the Unlock is called in all cases. – iPath ツ Oct 23 '14 at 09:52

1 Answers1

2

The enumerator is created at the start of the for loop, and destroyed when the loop ends. The lifetime of the enumerator is managed by a try/finally.

Don't just take my word for this though. It's easy enough to add some debugging code that will instrument your loop and let you see when the destructor is called.

This means that your proposed locking strategy is sound.

I would say though that allocating an enumerator on the heap, when you have thread contention, could cause performance problems.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Just curious: aren't enumerators always allocated on the heap or you mean using enumerators can cause perf issues in general? – iPath ツ Oct 23 '14 at 10:22
  • 2
    If your enumerator is a record it is stack allocated. All mine are. You could not use your lock though. Records don't have destructor. – David Heffernan Oct 23 '14 at 10:30