我有一个可以由多个线程调用的单例。
我经常对数据进行一些查找,并缓存数据,这样我就不必一次又一次地重复相同的查找。
我想做一些类似于使用静态局部变量的事情,但以线程安全的方式。我怀疑下面的代码是无懈可击的。这是对的吗?
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:此代码是否完全线程安全/可重入?
threadvar
实例都填充为零,因此您的string
变量已正确初始化。可悲的是,
threadvar
不处理其托管类型的内存... 因此,您需要释放Previous
变量中的每个string
。在实践中,我不将托管类型存储在 threadvar 中,而是使用另一种模式(如构造函数级别的注入)。
性能影响很小:访问每个
Previous.xxxxx
成员都有性能成本:您可能宁愿用@Previous
填充局部变量指针,然后使用此指针访问字段(或使用with Previous do
- 但此语法可能会令人困惑)。
所有线程变量,无论是否托管,初始化为零。请注意,当线程终止时,它们尚未完成,因此您需要负责。
没有数据争用,因为您使用线程变量。我不确定当你问到重新进入时你的意思。代码中没有任何内容建议重入执行。
你已经对你的问题 A 有了合适的答案,所以我只关注"这是 threadvar threadsafe 的用法吗?"。我将首先考虑threadvar
线程的安全性。然后,我将在您的问题中对代码提出一些观察结果。
您提供的代码中的任何内容都不会引起任何警钟。但是,希望我提供的信息将帮助您自信地根据其余代码进行自己的评估。
严格来说,threadvar
不是线程安全的。threadvars 提供的唯一内容是每个线程的本地变量版本。每个线程都有自己的副本这一事实可能导致过度简化的错误逻辑:即假设数据是线程安全的。
线程局部变量的重要一点是每个线程都有自己的数据副本,因此不需要任何典型的线程安全机制。
但是,仍然可以通过线程变量共享数据。
- 不同线程中的对象变量可以引用共享对象。 字符串
- 变量可以引用共享字符串A。
- 指针变量可以引用相同的内存位置B。
- 甚至记录和基元类型的线程变量也可以通过获取一个线程变量的地址
@
并找到另一个线程的路径来共享。
鉴于上述情况,A 需要进一步考虑,因为您的记录具有字符串和指针。
如你所知。Delphi对长字符串的处理意味着字符串变量的数据可以很好地在多个线程之间共享。幸运的是,每个变量(包括每个线程变量实例)都将保存自己的引用计数。鉴于德尔福的 COW 和对 refcount 的联锁访问,您可以放心,字符串将是安全的......当然,假设你没有任何狡猾的代码绕过引用计数来操作字符串。
不过,Canvas: Pointer
不太清楚。我怀疑您可能只是为了指针本身而持有它。但是,如果您真的打算使用共享画布,则可能会遇到困难。
结论
线程- 变量实际上不是"线程安全的"。但通常它们不需要,因为根据意图,线程变量通常不会共享数据。
- 但是,线程变量可以以共享数据的方式使用。所以你仍然需要注意这种可能性。
- 最后,呈现的代码看起来很安全。但是您的最终答案可能取决于
Canvas
是否共享SomeCalculation
内部发生的事情。
作为旁白
您解释了缓存某些查找数据的意图。有一个严肃的选择值得考虑。如果共享/缓存数据驻留在不可变对象中,则任意数量的线程都可以安全地同时读取数据。铌但是,您确实需要保证没有其他线程可以同时修改数据。