当使用引用计数时,处理循环引用的可能解决方案/技术有哪些?
最为人所知的解决方案是使用弱引用,然而许多与此主题相关的文章表明还有其他方法,但它们不断重复弱引用的示例。这让我想知道,这些其他方法是什么?
我并不是在询问替代引用计数的方案,而是在使用引用计数时解决循环引用的方案。
这个问题不涉及任何特定的问题/实现/语言,而是一个普遍性的问题。
当使用引用计数时,处理循环引用的可能解决方案/技术有哪些?
最为人所知的解决方案是使用弱引用,然而许多与此主题相关的文章表明还有其他方法,但它们不断重复弱引用的示例。这让我想知道,这些其他方法是什么?
我并不是在询问替代引用计数的方案,而是在使用引用计数时解决循环引用的方案。
这个问题不涉及任何特定的问题/实现/语言,而是一个普遍性的问题。
多年来,我以各种方式研究了这个问题,唯一的解决方案是重新设计我的解决方案,不使用循环引用。
编辑:
你能详细说明吗?例如,在子项需要知道/访问父项的父子关系时,您将如何处理?– OB OB
正如我所说,除非您使用可以安全处理此类结构的运行时,否则避免使用这样的结构是唯一的好方法。
话虽如此,如果您必须拥有子节点了解父节点的树/父子数据结构,那么您将不得不手动实现自己的拆卸序列(即在任何析构函数之外调用),从根部(或您想要修剪的分支)开始对树进行深度优先搜索,以便从叶子中删除引用。
这变得复杂而繁琐,因此我认为唯一的解决方案是完全避免使用它。
这里是我见过的一种解决方案:
为每个对象添加一个方法,告诉它释放对其他对象的引用,比如称之为Teardown()。
然后您必须知道谁“拥有”每个对象,并且对象的所有者在完成对其的操作后必须调用Teardown()。
如果存在循环引用,比如A <-> B,并且C拥有A,则当调用C的Teardown()时,它会调用A的Teardown,这将调用B的Teardown,B然后释放其对A的引用,A然后释放其对B的引用(销毁B),然后C释放其对A的引用(销毁A)。
我猜垃圾回收器使用的另一种方法是“标记和清除”:
拥有弱引用是一种解决方案。我知道的另外一个解决方案是尽可能避免循环拥有引用。如果您拥有指向对象的共享指针,则从语义上来说,这意味着您以共享的方式拥有该对象。如果您只以这种方式使用共享指针,则几乎不可能出现循环引用。通常不会出现对象彼此循环拥有的情况,而是通过分层树状结构连接的。下面我将描述这种情况。
如果您有一个具有父子关系的对象树,则子对象不需要拥有其父对象的所有权引用,因为父对象总是会比子对象存活更长时间。因此,非所有权的原始反向指针就足够了。对于指向它们所在容器的元素也适用此规则。容器应尽可能使用唯一指针或值,而不是共享指针。
如果您有一堆可以随意相互引用的对象,并且您希望在某些对象不可达时立即清理它们,则您可能希望为它们构建一个容器和一个根引用数组,以便手动进行垃圾回收。
在现实世界中,我发现共享指针的实际用途非常有限,应该避免使用,而应优先选择唯一指针、原始指针或更好的只使用值类型。通常情况下,共享指针是在多个引用指向共享变量时使用的。共享会引起摩擦和争用,因此应尽可能避免使用它。唯一指针和非所有权的原始指针和/或值比较容易理解。但是,有时需要使用共享指针。通常使用共享指针来延长对象的生存期,这通常不会导致循环引用。
请谨慎使用共享指针。优先使用唯一指针、非所有权的原始指针或纯值。共享指针表示共享所有权。请按此方式使用。将您的对象按照层次结构排序。处于层次结构中的子对象或同级对象不应相互拥有共享所有权引用,也不应该拥有指向其父对象的共享所有权引用,而应使用非所有权的原始指针代替。
我曾经写过一种编程语言,其中每个对象都是不可变的。因此,一个对象只能包含指向早期创建的对象的指针。由于所有引用都指向过去,因此不可能存在任何循环引用,并且引用计数是管理内存的完全可行的方法。
那么问题来了,“如何创建自引用的数据结构而不产生循环引用?”好吧,函数式编程一直在使用固定点/Y组合子来编写递归函数,当您只能引用先前编写的函数时。请参见:https://en.wikipedia.org/wiki/Fixed-point_combinator
那个维基百科页面很复杂,但概念并不那么复杂。就数据结构而言,它的工作原理如下:
假设您想要创建一个Map<String,Object>
,其中映射中的值可以引用映射中的其他值。那么,您不是实际存储这些对象,而是存储函数,这些函数会根据给定映射的指针按需生成这些对象。
object = map.get(key)
,这将调用一个私有方法 _getDefinition(key)
,该方法返回此函数并调用它。Object get(String key) {
var definition = _getDefinition(key);
return definition == null ? null : definition(this);
}
因此,该映射具有对定义值函数的引用。该函数不需要引用映射,因为它将作为参数传递映射。
当调用该定义时,它返回一个新对象,该对象确实在某个地方具有指向映射的指针,但是映射本身没有指向此对象的指针,因此不存在循环引用。
一些论文: "Down for the Count? Getting Reference Counting Back in the Ring" Rifat Shahriyar, Stephen M. Blackburn and Daniel Frampton http://users.cecs.anu.edu.au/~steveb/downloads/pdf/rc-ismm-2012.pdf "A Unified Theory of Garbage Collection" by David F. Bacon, Perry Cheng and V.T. Rajan http://www.cs.virginia.edu/~cs415/reading/bacon-garbage.pdf
参考计数还有很多其他主题,例如减少或消除参考计数中交错指令的奇特方法。我可以想到三种方法,其中两种已经被写出。
我也在寻找解决循环引用计数问题的好方法。
我从魔兽世界中借鉴了一个处理成就的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;
IAchievementCriteria = interface(IUnknown)
function GetDescription: string;
...
procedure Notify(eventType: Integer; gParam: TGUID; nParam: Integer);
end;
到目前为止,依赖图一直都是自顶向下的:
IAchievementController --> IAchievement --> IAchievementCriteria
IAchievementController --> IAchievement --> IAchievementCriteria
^ |
| |
+----------------------+
Criteria
需要引用其父级;现在它们互相引用- 内存泄漏。 IAchievementController --> IAchievement --> IAchievementCriteria
^ | ^ |
| | | |
+----------------------+ +----------------------+
现在Controller
和它的子类Achievements
相互引用,导致更多的内存泄漏。
我认为也许Criteria
对象可以通知Controller
,从而删除对其父级的引用。但我们仍然有一个循环引用,只是时间更长了:
IAchievementController --> IAchievement --> IAchievementCriteria
^ | |
| | |
+<---------------------+ |
| |
+-------------------------------------------------+
现在,魔兽世界的 API 并不友好于面向对象编程。但它确实解决了任何循环引用的问题:
不要传递对Controller
的引用。有一个单一的、全局的、单例的Controller
类。这样,成就就不必“引用”控制器,只需“使用”它。
缺点:使测试和模拟变得不可能——因为你必须有一个已知的全局变量。
成就不知道它的标准列表。如果你想要Achievement
的Criteria
,你可以向Controller
询问:
IAchievementController = interface(IUnknown)
function GetAchievementCriteriaCount(AchievementGUID: TGUID): Integer;
function GetAchievementCriteria(Index: Integer): IAchievementCriteria;
end;
缺点:一个Achievement
不能再决定将通知传递给它的Criteria
,因为它没有任何标准。现在你必须向Controller
注册Criteria
。
当一个Criteria
完成时,它会通知Controller
,然后Controller
会通知Achievement
:
IAchievementController-->IAchievement IAchievementCriteria
^ |
| |
+----------------------------------------------+
缺点:让我头疼。
我相信,与将整个系统重新架构成一个非常混乱的 API 相比,使用 Teardown
方法肯定更加理想。
但是,就像你在思考的那样,也许有更好的方法。
如果您需要存储循环数据,以便将其快照到字符串中,
我会将一个循环布尔值附加到任何可能是循环的对象上。
步骤1: 在将数据解析为JSON字符串时,我将任何未使用过的object.is_cyclic推入数组并将索引保存到字符串中。(任何已使用的对象都将替换为现有索引)。
步骤2:我遍历对象数组,将任何children.is_cyclic设置为指定的索引,或将任何新对象推入数组。然后将数组解析为JSON字符串。
注意:通过将新的循环对象推到数组末尾,将强制递归,直到删除所有循环引用为止。
步骤3:最后,我将两个JSON字符串解析为单个字符串;
这里是一个JavaScript fiddle... 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)
});
}
}
}
我一直在重新设计以避免这个问题。其中一个常见的情况是父子关系,其中子项需要了解父项。有两种解决方案:
将父项转换为服务,然后父项不再知道子项,当没有更多子项或主程序删除父项引用时,父项就会死亡。
如果父项必须访问子项,则在父项上有一个注册方法,该方法接受一个指针,该指针不是引用计数的,例如对象指针,以及相应的注销方法。子项将需要调用注册和注销方法。当父项需要访问子项时,它将把对象指针强制转换为引用计数接口。