首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > .NET > .NET >

讨论上lazy-loading和线程安全

2013-01-01 
讨论下lazy-loading和线程安全大家一定听说过lazy-loading(延迟加载),简单来说,延迟加载就是只有当需要某

讨论下lazy-loading和线程安全
大家一定听说过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;




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

如有任何错误,请一定指出:)
[解决办法]
这个贴子不错
[解决办法]
学习
[解决办法]
楼主最好把测试数据列上,同样的代码,不同的同步方法


临界区 成本最高的 ,这句话不认同, 临界区比通过消息,事件等的效率都高,
[解决办法]
倾向使用第4种
[解决办法]
支持下,用过第二种
[解决办法]

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


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


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

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

热点排行