Delphi中多线程传递字符串后修改是否安全?
Delphi多线程中修改线程自有字符串变量是否安全?
我知道Delphi中的字符串采用引用计数机制,引用计数本身是原子操作,但字符串变量本身不是。也清楚在不同线程中读写同一个字符串变量时必须使用锁。但疑问是:在线程构造阶段将一个字符串传递给多个线程,启动线程后,线程内部修改自己持有的这个字符串是否安全?
代码示例
type TThreadTest = class(TThread) public FStr: string; protected procedure Execute; override; end; { TThreadTest } procedure TThreadTest.Execute; begin inherited; var i: Int64 := 0; while not Terminated do begin Inc(i); // 线程内部修改自己的FStr FStr := i.ToString; Sleep(1); end; end; // 全局字符串变量 var G_Str: string := 'test1'; // 主线程中给线程1复制字符串引用 var Thread1 := TThreadTest.Create(True); Thread1.FStr := G_Str; // 主线程中给线程2复制字符串引用 var Thread2 := TThreadTest.Create(True); Thread2.FStr := G_Str; // 清除全局变量的引用,此时Thread1和Thread2的FStr指向同一个字符串实例 G_Str := 'other'; // 启动两个线程 Thread1.Start; Thread2.Start;
调试观察到的字符串赋值逻辑
在x64调试器中,Thread1.FStr := G_Str;会调用System.pas中的_UStrAsg函数,该函数负责字符串的引用计数更新和指针赋值:
procedure _UStrAsg(var Dest: UnicodeString; const Source: UnicodeString); var S, D: Pointer; P: PStrRec; Len: Integer; begin S := Pointer(Source); if S <> nil then begin if __StringRefCnt(Source) < 0 then begin // 处理不可共享的字符串(比如常量字符串),复制新实例 Len := __StringLength(Source); S := _NewUnicodeString(Len); Move(Pointer(Source)^, S^, Len * SizeOf(WideChar)); end else begin // 对可共享字符串,原子增加引用计数 P := PStrRec(PByte(S) - SizeOf(StrRec)); AtomicIncrement(P.refCnt); end; end; D := Pointer(Dest); // 将源字符串的指针赋值给目标变量 Pointer(Dest) := S; if D <> nil then begin // 原子减少原目标字符串的引用计数,为0则释放内存 P := Pointer(PByte(D) - SizeOf(StrRec)); if (P.refCnt > 0) and (AtomicDecrement(P.refCnt) = 0) then FreeMem(P); end; end;
结论:线程内部修改自有字符串变量是安全的
核心原因如下:
- 线程构造阶段的字符串赋值操作都在主线程中执行,
_UStrAsg的引用计数原子操作保证了这一步的安全性,最终Thread1和Thread2的FStr指向同一个字符串实例,引用计数为2。 - 线程启动后,每个线程操作的是自己的FStr成员变量:这是每个线程独有的变量,不存在多个线程同时读写同一个变量的情况。
- 当线程执行
FStr := i.ToString时,本质是对自己的FStr变量进行赋值:- 先创建新的字符串实例(
i.ToString),初始引用计数为1; - 调用
_UStrAsg将新字符串的指针赋值给自己的FStr,同时原子递减原有字符串的引用计数(此时原有字符串的引用计数从2变为1,不会被释放)。
- 先创建新的字符串实例(
- 整个过程中,没有多个线程竞争同一个字符串变量,也不会修改共享的字符串实例(Delphi字符串是不可变的,赋值会生成新实例),因此不需要加锁,操作是安全的。
内容的提问来源于stack exchange,提问作者qgi
相关产品推荐
相关产品推荐

