TStringList是线程安全的吗?

3

在没有任何形式的同步措施,例如与主线程同步,从TStringList读取数据是否安全?

示例代码:

var MyStringList:TStringList; //declared globally

procedure TForm1.JvThread1Execute(Sender: TObject; Params: Pointer);
var x:integer;
begin

   for x:=0 to MaxInt do MyStringList.Add(FloatToStr(Random));  

end;


procedure TForm1.ButtonClick(Sender: TObject);
var x:integer;
   SumOfRandomNumbers:double;
begin

  for x:=0 to MyStringList.Count-1 do
    SumOfRandomNumbers:=SumOfRandomNumbers+StrToFloat(MyStringList.Strings[x]);

end;

或者我应该使用EnterCriticalSection来保护对MyStringList的访问。
var MyStringList:TStringList; //declared globally

procedure TForm1.JvThread1Execute(Sender: TObject; Params: Pointer);
var x:integer;
begin

   for x:=0 to MaxInt do 
   begin
     EnterCriticalSection(MySemaphore); 
     MyStringList.Add(FloatToStr(Random));  
     LeaveCriticalSection(MySemaphore);
   end;

end;


procedure TForm1.ButtonClick(Sender: TObject);
var x:integer;
   SumOfRandomNumbers:double;
begin

  for x:=0 to MyStringList.Count-1 do
  begin

     EnterCriticalSection(MySemaphore);
     SumOfRandomNumbers:=SumOfRandomNumbers+StrToFloat(MyStringList.Strings[x]);
     LeaveCriticalSection(MySemaphore);

  end;

end;

1
你必须始终保护可以同时写入和读取的内存。在你的情况下,主线程可以读取,而辅助线程可以写入,因此是的,你需要进行同步。 - whosrdaddy
2
怎么可能呢?编译器得读懂你的思想。加上同步。 - David Heffernan
2个回答

16

首先,TStringList 不是线程安全的。
其次,试图使其线程安全对于低级容器来说是可怕的想法,在绝大多数情况下,它不会在多个线程之间共享。
第三,您提出的使其线程安全的 naive 代码远远不足够。它远远不能使其真正线程安全——这是尝试以通用方式实现线程安全性的问题的一部分。

在您的问题文本中,您问道:

在没有任何形式的同步的情况下从 TStringList 中读取数据是否可以?

是可以的。实际上,这更有效率。

但是,如果数据跨线程共享,则可能会遇到问题。这就是为什么应该尽量减少跨线程共享的数据(不仅仅是字符串列表)。如果确实需要共享数据,请以适当受控制的方式进行。


关于第三点的详细说明

你的代码之所以不是线程安全的,是因为它没有完全保护你的所有数据免受共享访问。这是多线程开发中常见的误解:“我只需要使用锁包装某些操作,一切都会很好。

关键是,如果你的列表是共享的,你在:

  • 共享表示容器的结构。
  • 并且你正在共享数据成员(实际的字符串)本身。
  • 当涉及到字符串时,这更进一步,因为 Delphi 管理字符串的方式意味着它们可以与应用程序的完全不同区域中具有相同值的其他字符串(通过内部引用计数)进行共享。

虽然你提出的锁定策略可能适用于你当前的要求,但它远远不能通用地实现线程安全。

结论

如果您想编写线程安全的代码,则责任在您自己,需要:

  • 了解数据访问路径。
  • 尽量减少线程之间的共享(这是最划算的方法)。
  • 实现最佳安全共享数据策略(有许多选项可供选择,而锁定并不保证在任何情况下都是最佳选择)。

旁注

我之前提到您的锁定技术仅“可能适合您当前的要求”,因为我认为您并没有真正说明您的真实需求。如果您已经这样做了,那么您确实需要注意以下内容

在您提供的代码中,使您的TStringList“线程安全”毫无益处。您在循环中填充列表,然后在第二个循环中读取值。您并未采取任何措施以同时使用数据。

您的代码应尽可能远离多线程:最好将两个循环处理放在主线程之外,以避免阻止UI。在这种情况下,后台线程不应共享其TStringList实例。它可以简单地与主线程同步以报告结果(和可能的进度更新)。

通过不共享不需要共享的数据,您可以完全避开锁定的需求。锁定将成为一个不必要的开销。您可以放心,TStringList没有内置的“线程安全”机制。


1
不,它并不是。在TStringList内部没有机制锁定例如.Add()或.GetStrings()。很遗憾,没有像TThreadList这样的内置机制,它是TList的线程安全包装器。但你可以很容易地自己构建它。这里有一个简单的示例,用于TStringList的同步修饰器,在其中我涵盖了Add()的情况:
TThreadStringList = class
private
  FStringList: TStringList;
  FCriticalSection: TRtlCriticalSection;
  // ...
public 
  function Add(const S: string): Integer;
  // ...
end;

// ...

TThreadStringList.Add(const S: string): Integer;
begin
  EnterCriticalSection(FCriticalSection);
  try
    Result:= Add(S);
  finally
    LeaveCriticalSection(FCriticalSection);
  end;
end;

这应该很容易,可以将其应用到您需要的所有其他方法中。

请记住,在使用关键部分之前,您必须初始化它,并在之后删除它。


问题中基于临界区的代码将锁放置在错误的位置。 - David Heffernan
@DavidHeffernan 是的,我添加了一个示例来澄清。 - SevenEleven
仍然不起作用。您不能仅锁定每个方法并使所有代码线程安全。 - David Heffernan
为什么不呢?我不建议锁定每个(!)方法。但是根据用例提供那些真正需要锁定的方法的接口。例如,在这里,它将是Add()SetStrings()GetStrings() - SevenEleven
2
因为一些需要原子操作的操作可能跨越多个方法调用。对于复杂类型的内部同步很少可行。通常使用情况决定了适当的同步策略。 - David Heffernan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接