这是使用threadvar线程安全吗?

时间:2017-05-14 19:10:57

标签: string delphi thread-safety

我有一个可以被多个线程调用的单例 我经常对数据进行一些查询,然后我要缓存数据,这样我就不必一次又一次地重复相同的查找。

我想做一些类似于使用静态局部变量的东西,但是以线程安全的方式。我怀疑下面的代码是不漏水的。这是对的吗?

type
  TPrevious = record
  public
    Fontname: string;
    FontSize: integer;
    Canvas: pointer;
    Width: integer;
  end;

threadvar Previous: TPrevious;

function TEditorOptions.GetEditorFontWidth(const Canvas: TCanvas): integer;
var
  Font: TFont;
//var  //static vars            <<-- static var != threadsafe
//  PreviousFontName: string = '';
//  PreviousFontSize: integer = 0;
//  PreviousCanvas: pointer = nil;
//  PreviousWidth: integer = 0;
begin
  {1: I'm assuming a managed threadvar is always initialized to Default(T)}
  if (Previous.Fontname <> '') then begin
      //Cache the values, so we don't recalculate all the time.
      //Caching is per thread, but that's fine.
    if (SameText(Previous.FontName, FFontName)) and (Previous.FontSize = FFontSize)
       and (pointer(Canvas) = Previous.Canvas) then Exit(Previous.Width);
  end;
  Previous.Canvas := pointer(Canvas);
  Previous.FontName := FFontName;
  Previous.FontSize := FFontSize;
  Result:= SomeCalculation(Canvas, FFontName, FFontSize);
  ....
    Previous.Width:= Result;
  ....
end;

我有两个问题:

答:我是否认为像字符串FontName这样的托管线程变量始终初始化为Default(T)(即''

B:这段代码是完全线程安全/可重入吗?

3 个答案:

答案 0 :(得分:5)

  1. 任何threadvar个实例都填零,因此您的string变量已正确初始化。

  2. 可悲的是,threadvar无法处理其托管类型的内存... 因此,您需要释放string变量中的每个Previous

  3. 实际上,我不会在threadvar中存储托管类型,而是使用其他模式(比如构造函数级别的注入)。

  4. 性能低下:对每个Previous.xxxxx成员的访问都会产生性能成本:您可能会使用@Previous填充局部变量指针,然后使用此指针访问字段(或使用with Previous do - 但这种语法可能令人困惑。)

答案 1 :(得分:3)

所有线程变量(无论是否受管)都是零初始化。请注意,当线程终止时它们没有最终确定,因此您需要负责。

由于您使用了线程var,因此没有数据争用。当你问起重新入职时,我不确定你的意思。代码中没有任何内容表明重新执行。

答案 2 :(得分:2)

你已经对问题A有了合适的答案,所以我只关注“这是否使用了threadvar线程安全?”。我首先考虑threadvar的线程安全性。然后我会就你问题中的代码发表一些意见。

您提供的代码中没有任何内容会引发任何警钟。但是,希望我提供的信息可以帮助您根据代码的其余部分自信地进行评估。

严格来说,threadvar 是线程安全的。 threadvars提供的唯一东西是每个线程本地变量的一个版本。每个线程都有自己的副本这一事实可能会导致 false-logic 过度简化:即假设数据是线程安全的。

线程局部变量的重点是每个线程都有自己的数据副本,因此不需要任何典型的线程安全机制

然而 仍然可以通过线程变量共享数据

  • 不同线程中的对象变量可以引用共享对象。
  • 字符串变量可以引用共享字符串 A
  • 指针变量可以引用相同的内存位置 B
  • 甚至记录和原始类型的线程变量也可以通过获取一个线程变量的地址@并找到另一个线程的方式来共享。

鉴于上述情况,A并需要进一步考虑因为你的记录有一个字符串和一个指针。

众所周知。 Delphi对长字符串的处理意味着字符串变量的数据可以很好地在多个线程之间共享。幸运的是,每个变量(包括每个threadvar实例)都将拥有自己的引用计数。鉴于Delphi的COW和对引用计数的互锁访问,您可以放心,该字符串将是安全的...... 当然,假设您没有任何狡猾的代码绕过引用计数来操纵字符串。

Canvas: Pointer虽然不太清楚。我怀疑你可能只是为指针本身拿着它。但如果您真的打算使用共享画布,则可能会遇到困难。

结论

  • 线程变量实际上并不是“线程安全的”。但通常他们不需要,因为通过意图线程文件通常不共享数据。
  • 但是,可以使用threadvars ,以便共享数据。所以你仍然需要注意这种可能性。
  • 最后,提供的代码看起来很安全。但您的最终答案可能取决于Canvas是否与SomeCalculation内的内容共享。

作为旁白

您解释了缓存某些查找数据的意图。有一个值得考虑的重要选择。如果共享/缓存数据驻留在不可变对象中,则任意数量的线程都可以安全地同时读取数据。 NB 您确实需要保证其他线程不能同时修改数据。