读书人

讨论下lazy-loading和线程安全解决方案

发布时间: 2012-03-14 12:01:12 作者: rapoo

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

Delphi(Pascal) code
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;


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

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

Delphi(Pascal) code
// 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,示例代码如下,大家可以比较下:

Delphi(Pascal) code
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指令直接对内存操作。

Delphi(Pascal) code
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执行一个比较和交换指针的原子操作,交换失败则销毁局部实例。代码如下:
Delphi(Pascal) code
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;



还有第四种方法,使用读写锁,尤其适用于读操作较多而写操作较少的情景:
Delphi(Pascal) code
// 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; 




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

如有任何错误,请一定指出:)

[解决办法]
这个贴子不错
[解决办法]
学习
[解决办法]
楼主最好把测试数据列上,同样的代码,不同的同步方法
临界区 成本最高的 ,这句话不认同, 临界区比通过消息,事件等的效率都高,
[解决办法]
倾向使用第4种
[解决办法]
支持下,用过第二种
[解决办法]
探讨
引用:
上面二种在高并发的情况下,上面的应该好过下面这个吧


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

读书人网 >.NET

热点推荐