讨论下lazy-loading和线程安全

ZuoBaoquan 2009-10-09 10:45:20
加精
大家一定听说过lazy-loading(延迟加载),简单来说,延迟加载就是只有当需要某资源的时候才去加载,以减少不必要的系统开销。如下面的代码所示,仅在访问Members属性的时候创建列表(而不是在constructor内创建)


type
TPerson = class
end;

TMembers = TObjectList<TPerson>;

TGroup = class
private
fMembers: TMembers;
function GetMembers: TMembers;
public
destructor Destroy; override;
//...
property Members: TMembers read GetMembers;
end;

//...

destructor TGroup.Destroy;
begin
fMembers.Free;
inherited Destroy;
end;

function TGroup.GetMembers: TMembers;
begin
if fMembers = nil then
begin
fMembers := TMembers.Create;
end;
Result := fMembers;
end;



我们现在来讨论一下,如果要保证上面的这段代码是线程安全的,有哪些方法,以及各种方法有什么利弊。我先把自己见过的方法列举一下,欢迎大家讨论、补充:

第一种,完全通过临界区来保证该方法是线程安全的,如下列代码所示:



// fCriticalSection: SyncObjs.TCriticalSection;

constructor TGroup.Create;
begin
inherited Create;
fCriticalSection := TCriticalSection.Create;
end;

destructor TGroup.Destroy;
begin
fMembers.Free;
fCriticalSection.Free;
inherited Destroy;
end;

function TGroup.GetMembers: TMembers;
begin
fCriticalSection.Enter;
try
if fMembers = nil then
begin
fMembers := TMembers.Create;
end;
Result := fMembers;
finally
fCriticalSection.Leave;
end;
end;


呵呵,这就是我刚接触多线程的时候用的方法(类似java里面的synchronized方法)。应该说,这种方法最简单,最容易理解,当然也是成本最高的。(其实所谓的线程安全,实际上是对共享资源的保护,并不需要完全同步方法,以免浪费系统资源。)

第二种方法,即所谓的double checked locking,常见于c#和java,示例代码如下,大家可以比较下:



function TGroup.GetMembers: TMembers;
begin
if fMembers = nil then // 先判断fMembers是否为nil,若为nil才进入临界区
begin
fCriticalSection.Enter;
try
if fMembers = nil then // 再次判断fMembers(这就是double-checked locking的由来)
begin
fMembers := TMembers.Create;
end;
finally
fCriticalSection.Leave;
end;
end;
Result := fMembers;
end;


能想出这种方法的人绝对聪明。但这里必须指出,这段代码有个隐藏的问题。什么问题呢?(在C#里面必须把变量声明为volatile才可以用double-checked locking。)查了下MSDN的帮助下才算弄明白了一点。(推荐阅读MSDN: Synchronization and Multiprocessor Issues)

原因在于*处理器在读、写内存的时候会使用缓存以便优化性能*。假设有两个线程分别在两个处理器上执行这段代码,线程A进入了临界区并把创建好的实例赋值给(缓存中的)fMembers后退出了临界区。接下来处理器B上运行的线程B进入了临界区,当它判断fMembers的时候,由于缓存中的fMembers还没有更新,于是又创建了一次。杯具啊。。。

那怎么解决呢?System里面有一个MemoryBarrier函数可以保证CPU指令直接对内存操作。



function TGroup.GetMembers: TMembers;
begin
if fMembers = nil then
begin
fCriticalSection.Enter;
try
MemoryBarrier;
if fMembers = nil then
begin
fMembers := TMembers.Create;
end;
finally
fCriticalSection.Leave;
end;
end;
Result := fMembers;
end;



btw. 是不是第一种方法也需要加MemoryBarrier?

第三种方法,也是VCL里面使用的比较多的,即先判断fMembers是否为nil,若为nil则创建一个局部实例,再使用InterlockedCompareExchangePointer执行一个比较和交换指针的原子操作,交换失败则销毁局部实例。代码如下:


function TGroup.GetMembers: TMembers;
var
list: TMembers;
begin
if fMembers = nil then
begin
list := TMembers.Create;
if InterlockedCompareExchangePointer(fMembers, list, nil) <> nil then
begin
list.Free;
end;
end;
Result := fMembers;
end;




还有第四种方法,使用读写锁,尤其适用于读操作较多而写操作较少的情景:


// fMembersSync: SysUtils.IReadWriteSync;

constructor TGroup.Create;
begin
inherited Create;
fMembersSync := TMREWSync.Create; // or TMultiReadExclusiveWriteSynchronizer.Create;
end;

destructor TGroup.Destroy;
begin
fMembers.Free;
inherited Destroy;
end;

function TGroup.FindMember(const name: string): TPerson;
var
person: TPerson;
begin
fMembersSync.BeginRead; // 查找成员是“读操作”
try
for person in Members do
begin
if SameText(person.Name, name) then
begin
Result := person;
Break;
end;
end;
finally
fMembersSync.EndRead;
end;
end;

procedure TGroup.AddMember(person: TPerson);
begin
fMembersSync.BeginWrite; // 添加成员属于“写操作”
try
Members.Add(person);
finally
fMembersSync.EndWrite;
end;
end;

// 这里用读写锁应该没有意义,是不是要用上面的某种方法来保证线程安全???
function TGroup.GetMembers: TMembers;
begin

end;




呵呵,我就抛砖引玉到这儿了,下面欢迎大家热烈讨论,我会把讨论的结果总结后放到博客上。

如有任何错误,请一定指出:)
...全文
715 25 打赏 收藏 转发到动态 举报
写回复
用AI写文章
25 条回复
切换为时间正序
请发表友善的回复…
发表回复
budded 2012-08-03
  • 打赏
  • 举报
回复
请问如何在低版本中使用InterlockedCompareExchangePointer?
jia7007 2009-10-10
  • 打赏
  • 举报
回复
坐看,
madman007 2009-10-10
  • 打赏
  • 举报
回复
不错,加油
ZuoBaoquan 2009-10-10
  • 打赏
  • 举报
回复
[Quote=引用 8 楼 starluck 的回复:]
上面是判断没有建立,就强制同步结程一致辞,如果还是判断没有,才建立?
[/Quote]
这就是double-checked lock的由来

[Quote=引用 8 楼 starluck 的回复:]
"使用 MemoryBarrier 生成正确的多线程程序是非常困难的。在大多数情况下,C# 中的 lock 语句、Visual Basic 中的 SyncLock 语句和 Monitor 类的方法都提供了更简单的且不容易出错的方式来同步内存访问。建议您使用这些语句和方法,而不要使用 MemoryBarrier。"

这上面是MSDN上的一段话,搞得我有点犯迷糊了.
[/Quote]
关于MemoryBarrier是否能用以及怎么用还要再研究下。

[Quote=引用 8 楼 starluck 的回复:]
上面二种在高并发的情况下,上面的应该好过下面这个吧
[/Quote]
感觉上是这样的(高并发的情况下,第二种比第三种要好一点)。不过因为两种方法都先判断fMembers是否为nil,一般也很难体现出来。
ZuoBaoquan 2009-10-10
  • 打赏
  • 举报
回复
[Quote=引用 17 楼 acrodelphi 的回复:]
有营养。多线程中确实有很多需要注意的陷阱。
MemoryBarrier在delphi7中没有找到。
[/Quote]
应该是D2009加入的,System.pas
procedure MemoryBarrier;
asm
PUSH EAX
XCHG [ESP],EAX
POP EAX
end;
ZuoBaoquan 2009-10-10
  • 打赏
  • 举报
回复
[Quote=引用 16 楼 dinoalex 的回复:]
free 本身不就是检查是否为nil吗?

procedure GetMembers;
var
  newm: TMembers;
begin
  newm:= TMembers.Create;
  fMember.free;
  fMember:= newm;
end;
[/Quote]
这个是什么意思?
火星牛 2009-10-10
  • 打赏
  • 举报
回复
有营养。多线程中确实有很多需要注意的陷阱。
MemoryBarrier在delphi7中没有找到。
dinoalex 2009-10-10
  • 打赏
  • 举报
回复
free 本身不就是检查是否为nil吗?

procedure GetMembers;
var
newm: TMembers;
begin
newm:= TMembers.Create;
fMember.free;
fMember:= newm;
end;
starluck 2009-10-10
  • 打赏
  • 举报
回复
[Quote=引用 20 楼 zuobaoquan 的回复:]
引用 8 楼 starluck 的回复:
上面二种在高并发的情况下,上面的应该好过下面这个吧


感觉上是这样的(高并发的情况下,第二种比第三种要好一点)。不过因为两种方法都先判断fMembers是否为nil,一般也很难体现出来。
[/Quote]

如果在双核的情况下,高并发,第三种应该是更好。

MSDN上的说法我想应该是很少有这种并发需求,才说的那段话吧。

Harryfin 2009-10-09
  • 打赏
  • 举报
回复
支持下,用过第二种
starluck 2009-10-09
  • 打赏
  • 举报
回复

function TGroup.GetMembers: TMembers;
begin
if fMembers = nil then
begin
fCriticalSection.Enter;
try
MemoryBarrier;
if fMembers = nil then
begin
fMembers := TMembers.Create;
end;
finally
fCriticalSection.Leave;
end;
end;
Result := fMembers;
end;

上面是判断没有建立,就强制同步结程一致辞,如果还是判断没有,才建立?

"使用 MemoryBarrier 生成正确的多线程程序是非常困难的。在大多数情况下,C# 中的 lock 语句、Visual Basic 中的 SyncLock 语句和 Monitor 类的方法都提供了更简单的且不容易出错的方式来同步内存访问。建议您使用这些语句和方法,而不要使用 MemoryBarrier。"

这上面是MSDN上的一段话,搞得我有点犯迷糊了.

function TGroup.GetMembers: TMembers;
var
list: TMembers;
begin
if fMembers = nil then
begin
list := TMembers.Create;
if InterlockedCompareExchangePointer(fMembers, list, nil) <> nil then
begin
list.Free;
end;
end;
Result := fMembers;
end;

上面二种在高并发的情况下,上面的应该好过下面这个吧?





ZuoBaoquan 2009-10-09
  • 打赏
  • 举报
回复
[Quote=引用 5 楼 liangpei2008 的回复:]
倾向使用第4种
[/Quote]
理由呢?对于上面的场景读写锁是有用的,但我的疑问是,GetMembers方法里面如果要用延迟加载的话,好像读写锁就没有用了,只有用其他的三种方法来同步,不知道是不是这样?
ZuoBaoquan 2009-10-09
  • 打赏
  • 举报
回复
[Quote=引用 4 楼 sanguomi 的回复:]
楼主最好把测试数据列上,同样的代码,不同的同步方法
[/Quote]
建议不错,抽时间做一下

[Quote=引用 4 楼 sanguomi 的回复:]
临界区 成本最高的 ,这句话不认同, 临界区比通过消息,事件等的效率都高,
[/Quote]
我的原意是第一种方法一开始就进入临界区,这样会造成其他的线程一直在等待,相比较其他的同步方法,显得效率不高。
liangpei2008 2009-10-09
  • 打赏
  • 举报
回复
倾向使用第4种
sanguomi 2009-10-09
  • 打赏
  • 举报
回复
楼主最好把测试数据列上,同样的代码,不同的同步方法
临界区 成本最高的 ,这句话不认同, 临界区比通过消息,事件等的效率都高,
ZuoBaoquan 2009-10-09
  • 打赏
  • 举报
回复
两位GG表个态啊~ 我已经搬了板凳等着听课呢
bdmh 2009-10-09
  • 打赏
  • 举报
回复
学习
liangpei2008 2009-10-09
  • 打赏
  • 举报
回复
这个贴子不错
simonhehe 2009-10-09
  • 打赏
  • 举报
回复
学习
myhder 2009-10-09
  • 打赏
  • 举报
回复
不错不错
加载更多回复(3)

16,748

社区成员

发帖
与我相关
我的任务
社区描述
Delphi 语言基础/算法/系统设计
社区管理员
  • 语言基础/算法/系统设计社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧