循环引用有哪些解决方案?

时间:2009-07-01 14:11:30

标签: reference-counting circular-reference

使用引用计数时,处理循环引用的可能解决方案/技术是什么?

最着名的解决方案是使用弱引用,但是有很多关于该主题的文章暗示还有其他方法,但不断重复弱引用示例。这让我想知道,这些其他方法是什么?

  • 我不是问什么是引用计数的替代方法,而是使用引用计数时循环引用的解决方案。

  • 这个问题不是关于任何具体问题/实施/语言,而是一般性问题。

11 个答案:

答案 0 :(得分:9)

多年来,我已经用十几种不同的方式查看了这个问题,我发现每次都能解决的唯一解决方案就是重新构建我的解决方案,而不是使用循环引用。

修改:

  

你可以扩展吗?例如,当孩子需要了解/访问父母时,您将如何处理父子关系? - OB OB

正如我所说,唯一的好解决方案是避免使用这样的结构,除非您使用的运行时可以安全地处理它们。

也就是说,如果你必须有一个树/父子数据结构,孩子知道父母,你将不得不实现自己的,手动调用的拆解序列(即你可能实现的任何析构函数的外部) )从根(或在你想要修剪的分支)开始,并对树进行深度优先搜索,以从树叶中删除引用。

它变得复杂和繁琐,因此IMO唯一的解决方案是完全避免它。

答案 1 :(得分:4)

这是我见过的解决方案:

为每个对象添加一个方法,告诉它释放对其他对象的引用,比如称之为Teardown()。

然后你必须知道谁拥有'每个对象,并且对象的所有者在完成它时必须在其上调用Teardown()。

如果存在循环引用,则说A< - > B和C拥有A,然后当调用C的Teardown()时,它调用A的Teardown,它在B上调用Teardown,B然后释放它对A的引用,A然后释放它对B的引用(销毁B),然后是C发布其对A(破坏A)的引用。

答案 2 :(得分:1)

我想垃圾收集器使用的另一种方法是“标记和扫描”:

  1. 在每个对象实例中设置一个标志
  2. 遍历每个可到达的实例的图表,清除该标志
  3. 仍然设置了标志的每个剩余实例都是无法访问的,即使其中一些实例具有相互循环引用。

答案 3 :(得分:1)

我想建议一个稍微不同的方法发生在我身上,我不知道它是否有任何正式名称:

自己的对象没有引用计数器。相反,一个或多个对象的组具有整个组的单个引用计数器,该计数器定义组中所有对象的生命周期。

以类似的方式,引用与对象共享组,或属于空组。

对象的引用只会影响(对象)组的引用计数,只要它是组的外部(引用)。

如果两个对象形成循环引用,则它们应成为同一组的一部分。如果两个组创建循环引用,则应将它们合并为一个组。

更大的群体允许更多的参考自由,但群体的对象更有可能在不需要的时候保持活力。

答案 4 :(得分:0)

我一直在重新设计以避免这个问题。出现这种情况的常见情况之一是父子关系,孩子需要知道父母的关系。这有两个解决方案

  1. 将父级转换为服务,父级不知道子级,父级在没有子级或主程序删除父级引用时死亡。

  2. 如果父级必须有权访问子级,则在父级上有一个寄存器方法,该方法接受未引用计数的指针,例如对象指针和相应的取消注册方法。孩子需要调用注册和取消注册方法。当父级需要访问子级时,它会键入将对象指针强制转换为引用计数接口。

答案 5 :(得分:0)

  

使用引用计数时,处理循环引用的可能解决方案/技术是什么?

三种解决方案:

  1. 使用循环检测器增加初始引用计数:递减到非零值的计数被认为是潜在的循环源,并且搜索它们周围的堆拓扑以寻找循环。

  2. 使用传统的垃圾收集器(如标记扫描)增加天真的引用计数。

  3. 限制语言使其程序只能生成非循环(也称为单向)堆。 Erlang和Mathematica这样做。

  4. 用字典查找替换引用,然后实现自己的垃圾收集器,可以收集周期。

答案 6 :(得分:0)

我也在寻找循环引用计数问题的良好解决方案。

我是偷窃从魔兽世界借用一个处理成就的API。当我意识到我有循环引用时,我隐含地将它翻译成接口。

  

注意:如果您不喜欢成就,可以将成就替换为订单。但谁不喜欢成就?

成就本身:

IAchievement = interface(IUnknown)
   function GetName: string;
   function GetDescription: string;
   function GetPoints: Integer;
   function GetCompleted: Boolean;

   function GetCriteriaCount: Integer;
   function GetCriteria(Index: Integer): IAchievementCriteria;
end;

然后是成就的标准列表:

IAchievementCriteria = interface(IUnknown)
   function GetDescription: string;
   function GetCompleted: Boolean;
   function GetQuantity: Integer;
   function GetRequiredQuantity: Integer;
end;    

所有成就都使用中央IAchievementController注册:

IAchievementController = interface
{
   procedure RegisterAchievement(Achievement: IAchievement);
   procedure UnregisterAchievement(Achievement: IAchievement);
}

然后可以使用控制器获取所有成就的列表

IAchievementController = interface
{
   procedure RegisterAchievement(Achievement: IAchievement);
   procedure UnregisterAchievement(Achievement: IAchievement);

   function GetAchievementCount(): Integer;
   function GetAchievement(Index: Integer): IAchievement;
}

这个想法是,当有趣的事情发生时,系统会调用IAchievementController通知他们发生了一些有趣的事情:

IAchievementController = interface
{
   ...
   procedure Notify(eventType: Integer; gParam: TGUID; nParam: Integer);
}

当发生事件时,控制器将遍历每个子节点并通过他们自己的Notify方法通知他们事件:

IAchievement = interface(IUnknown)
   function GetName: string;
   ...

   function GetCriteriaCount: Integer;
   function GetCriteria(Index: Integer): IAchievementCriteria;

   procedure Notify(eventType: Integer; gParam: TGUID; nParam: Integer);
end;

如果Achievement对象决定该事件是它感兴趣的事情,它将通知子标准:

IAchievementCriteria = interface(IUnknown)
   function GetDescription: string;
   ...
   procedure Notify(eventType: Integer; gParam: TGUID; nParam: Integer);
end;    

到目前为止,依赖图始终是自上而下的:

 IAchievementController --> IAchievement --> IAchievementCriteria

但是,当达到成就标准时会发生什么? Criteria对象必须通知其`成就:

 IAchievementController --> IAchievement --> IAchievementCriteria
                                    ^                      |
                                    |                      |
                                    +----------------------+

意味着Criteria需要对其父级的引用;谁现在互相引用 - 内存泄漏

当一项成就最终完成时,它将不得不通知其父控制器,因此它可以更新视图:

 IAchievementController --> IAchievement --> IAchievementCriteria
      ^                      |    ^                      |
      |                      |    |                      |                                        
      +----------------------+    +----------------------+

现在Controller及其子Achievements循环引用 - 更多内存泄漏

想到或许Criteria对象可以改为通知Controller,删除对其父级的引用。但是我们仍然有一个循环引用,只需要更长的时间:

 IAchievementController --> IAchievement --> IAchievementCriteria
      ^                      |                          |
      |                      |                          |                                        
      +<---------------------+                          |
      |                                                 |
      +-------------------------------------------------+

魔兽世界解决方案

现在魔兽世界 api 面向对象友好。但它确实解决了任何循环引用:

  1. 不要将引用传递给Controller。拥有一个单一的全局单例Controller类。这样,成就不必引用控制器,只需使用它。

    缺点:进行测试和嘲弄,不可能 - 因为 拥有已知的全局变量。

  2. 成就不知道其标准列表。如果您希望CriteriaAchievement,请向Controller询问他们:

     IAchievementController = interface(IUnknown)
         function GetAchievementCriteriaCount(AchievementGUID: TGUID): Integer;
         function GetAchievementCriteria(Index: Integer): IAchievementCriteria;
     end;
    

    缺点: Achievement无法再决定将通知传递给Criteria,因为它没有任何条件。您现在必须使用Criteria

  3. 注册Controller
  4. 完成Criteria后,会通知ControllerAchievement通知 IAchievementController-->IAchievement IAchievementCriteria ^ | | | +----------------------------------------------+

    Teardown

    缺点:让我的头受伤。

  5. 我确信将{{1}}方法重新设计成一个非常混乱的API,更加可取。{/ p>

    但是,就像你想知道的那样,也许有更好的方法。

答案 7 :(得分:0)

将事物放入层次结构

弱引用是一种解决方案。我所知道的唯一其他解决方案是避免循环拥有所有引用。如果您有共享指向对象的指针,那么这在语义上意味着您以共享方式拥有该对象。如果仅以这种方式使用共享指针,那么很难获得循环引用。对象通常不会以循环方式彼此拥有,而是通常通过分层树状结构连接。我将在下面描述这种情况。

处理树木

如果你的树上有一个父子关系的对象,那么孩子不需要对其父级的拥有引用,因为父级将会比这个子级更长。因此,非拥有的原始后退指针将执行。这也适用于指向它们所在容器的元素。如果可能的话,容器应尽可能使用唯一的指针或值而不是共享指针。

模拟垃圾收集

如果你有一堆对象可以疯狂地指向对方并且你想在一些对象无法访问时立即清理,那么你可能想要为它们构建一个容器并按顺序构建一个根引用数组手动进行垃圾收集。

使用唯一指针,原始指针和值

在现实世界中,我发现共享指针的实际用例非常有限,应该避免使用独特的指针,原始指针,或者 - 甚至更好 - 只是值类型。当您有多个指向共享变量的引用时,通常会使用共享指针。共享会导致摩擦和争用,如果可能的话,首先应该避免。唯一指针和非拥有原始指针和/或值更容易推理。但是,有时需要共享指针。共享指针也用于延长对象的生命周期。这通常不会导致循环引用。

底线

谨慎使用共享指针。首选独特的指针和非拥有的原始指针或普通值。共享指针表示共享所有权。以这种方式使用它们。在层次结构中排序对象。层次结构中同一级别的子对象或对象不应使用彼此或其父级的共享引用,而应使用非拥有的原始指针。

答案 8 :(得分:0)

没有人提到有一整套算法可以收集周期,而不是通过标记和扫描寻找不可收集的数据,而只是通过扫描一小组可能的循环数据,检测周期和收集他们没有完全扫除。

要添加更多细节,制作一组可能的扫描节点的一个想法是引用计数递减但在递减时没有变为零的那些。只有发生这种情况的节点才能成为从根集中切断循环的点。

Python有一个收集器可以做到这一点,就像PHP一样。

我仍然试图了解算法,因为有些高级版本声称能够在不停止程序的情况下并行执行此操作...

在任何情况下都不简单,它需要在“试验”中进行多次扫描,一组额外的参考计数器和递减元素(在额外的计数器中),以查看自引用数据是否最终是可收集的。

一些论文: “为伯爵打倒?获得参考计数”Rifat Shahriyar,Stephen M. Blackburn和Daniel Frampton http://users.cecs.anu.edu.au/~steveb/downloads/pdf/rc-ismm-2012.pdf  David F.Bacon,Perry Cheng和V.T.的“统一的垃圾收集理论”拉詹 http://www.cs.virginia.edu/~cs415/reading/bacon-garbage.pdf

在引用计数中有更多的主题,例如在引用计数中减少或消除互锁指令的奇特方法。我可以想到3种方法,其中2种已被写出来。

答案 9 :(得分:0)

如果需要存储循环数据,则将snapShot存储为字符串

我将循环布尔值附加到任何可能是循环的对象。

第1步: 在将数据解析为JSON字符串时,我将任何尚未使用的object.is_cyclic推送到数组中并将索引保​​存到字符串中。 (任何使用过的对象都替换为现有索引)。

步骤2:遍历对象数组,将所有children.is_cyclic设置为指定的索引,或将任何新对象推送到数组。然后将数组解析为JSON字符串。

注意:通过将新的循环对象推送到数组的末尾,将强制递归,直到删除所有循环引用。

第3步:最后我将两个JSON字符串解析为单个字符串;

这是一个javascript小提琴...... https://jsfiddle.net/7uondjhe/5/

function my_json(item) {

var parse_key = 'restore_reference', 
    stringify_key = 'is_cyclic';

var referenced_array = [];


var json_replacer = function(key,value) {

            if(typeof value == 'object' && value[stringify_key]) {
                var index = referenced_array.indexOf(value);

                if(index == -1) {
                    index = referenced_array.length;
                    referenced_array.push(value);
                };

                return {
                    [parse_key]: index
                }
            }
            return value;
}

var json_reviver = function(key, value) {

        if(typeof value == 'object' && value[parse_key] >= 0) {
                return referenced_array[value[parse_key]];
        }
        return value;
}
var unflatten_recursive = function(item, level) {
        if(!level) level = 1;
        for(var key in item) {
            if(!item.hasOwnProperty(key)) continue;
            var value = item[key];

            if(typeof value !== 'object') continue;

            if(level < 2 || !value.hasOwnProperty(parse_key)) {
                unflatten_recursive(value, level+1);
                continue;
            }

            var index = value[parse_key];
            item[key] = referenced_array[index];

        }
};


var flatten_recursive = function(item, level) {
        if(!level) level = 1;
        for(var key in item) {
            if(!item.hasOwnProperty(key)) continue;
            var value = item[key];

            if(typeof value !== 'object') continue;

            if(level < 2 || !value[stringify_key]) {
                flatten_recursive(value, level+1);
                continue;
            }

            var index = referenced_array.indexOf(value);

            if(index == -1) (item[key] = {})[parse_key] = referenced_array.push(value)-1; 
            else (item[key] = {})[parse_key] = index;
        }
};


return {

    clone: function(){ 
        return JSON.parse(JSON.stringify(item,json_replacer),json_reviver)
},

    parse: function() {
            var object_of_json_strings = JSON.parse(item);
            referenced_array = JSON.parse(object_of_json_strings.references);
            unflatten_recursive(referenced_array);
            return JSON.parse(object_of_json_strings.data,json_reviver);
    },

    stringify: function() {
            var data = JSON.stringify(item,json_replacer);
            flatten_recursive(referenced_array);
            return JSON.stringify({
                        data: data,
                        references: JSON.stringify(referenced_array)
                });
    }
}
}

答案 10 :(得分:-4)

我有几种方法可以解决这个问题:

第一个(也是首选的)只是将公共代码提取到第三个程序集中,并使两个引用都使用那个

第二个是将引用添加为“文件引用”(dll)而不是“项目引用”

希望这有帮助