线程保护对象列表

时间:2012-11-26 20:44:31

标签: multithreading delphi thread-safety delphi-xe2

我一直在使用一些多线程应用程序,其中一部分需要线程保护对象。我使用以下方法保护单个对象线程保护:

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;

但是,当我实现一个对象列表时,我无法弄清楚如何安全地保护每个对象。我创建了这样的对象列表:

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;

我认为有两种方法可以做到这一点,而且我不信任这两种方式。一种方法是在列表对象(TMyClasses)中实现关键部分锁定,并且其中的每个对象将共享此锁定(通过调用FOwner.Lock;FOwner.Unlock;。但是然后两个不同的线程将不会甚至可以同时使用此列表中的两个不同对象,并且会破坏多线程的目的。第二种方法是将另一个关键部分放在他们自己的每个单独对象中,但是其中太多是也很危险,对吗?如何保护列表和列表中的每个对象?

1 个答案:

答案 0 :(得分:1)

您无法真实地期望在列表类中使用与在序列化对单个对象的访问的简单类中使用相同的方法。

例如,您的列表类与之前的许多属性一样,具有Count属性和索引Items[]属性。我将假设你的线程模型允许列表变异。现在,假设你想编写这样的代码:

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

假设另一个线程在此循环运行时改变列表。那么,这显然会导致运行时故障。因此,我们可以得出结论,上面的循环需要用锁包装。这意味着列表的线程安全方面必须在外部公开。你无法将它全部保留在当前的设计中。

如果您希望将锁定保留在课程内部,则必须删除CountItems[]属性。你可以让你的列表看起来像这样(删除了一些部分):

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;

现在你可以用以下代码替换上面的循环:

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

根据Walk程序的确定,扩展此概念以允许您的Visit方法支持删除某些项目并不困难。

但正如你所说,你可以用这样的清单做的事情是没有实际意义的。共享数据是多线程的祸根。我建议你找到一种解决问题的方法,为每个线程提供所需数据的私有副本。此时你不需要同步,这一切都很好。

最后一点。线程安全没有单一的概念。线程安全的含义因环境而异。 Eric Lippert说得最好:What is this thing you call "thread safe"?所以当你提出这样的问题时,你应该详细说明你的特定用例和线程模型。