正确处理闭包中维护的引用以避免内存泄漏
Proper handling of maintained references in closures to avoid memory leaks
(前缀:对于那些看到这里并认为 TL;DR 的人,实际问题在最后)
自从在 C# 中发现了 lambda 和委托,我就成了它们的忠实用户。但是,在释放闭包中维护的对象上的内存时,我一直很担心,尤其是在处理嵌套闭包时。例如,考虑下面的一组 类 我正在写我认为是 "appropriate" INotifyPropertyChanged
行为的内容。
public static PropertyChangedEventHandler GetHandler<TDependant, TDependantHost, TFoundation, TFoundationHost>
(
this TDependantHost target,
PropertyChangedEventHandler invokeTarget,
Expression<Func<TDependantHost, TDependant>> dependantRef,
Expression<Func<TFoundationHost, TFoundation>> foundationRef,
Expression<Func<TDependantHost, TFoundationHost>> foundationHostRef
)
where TDependantHost : ISupportsDependencyManager
where TFoundationHost : class, INotifyPropertyChanged
{
string foundationName = GetPropertyInfo(foundationRef).Name;
string dependantName = GetPropertyInfo(dependantRef).Name;
string foundationHostName = GetPropertyInfo(foundationHostRef).Name;
Func<TDependantHost, TFoundationHost> foundationHostRefCompiled = foundationHostRef.Compile();
PropertyChangedEventHandler oOut = null;
// Complex situation. This is more complex because whilst TDependantHost bears a relationship to TFoundationHost
// the actual dependency is on a property in TFoundationHost.
// oOut is the property changed handler that will be attached to target, so it needs to
// - Raise changed events whenever foundationHostRef would evaluate to a different object
// - Whenever that change occurs, attach a new PropertyChangedEventHandler to the new foundationHost
// - ... which also handles removal of itself from target so as to guarantee
oOut = (s, e) =>
{
var sender = s as INotifyPropertyChanged;
if (sender == null)
return;
if (e.PropertyName == foundationHostName)
{
// The Foundation Host has changed. So we need to attach a new inner PropertyChangedEventHandler to it.
PropertyChangedEventHandler innerHandler = null;
innerHandler =
(s2, e2) =>
{
// Caller safety...
var innerSender = s2 as TFoundationHost;
if (innerSender == null)
return;
// Check and see if this eventhandler still points to the right object
// If it does, we'll keep going - otherwise, got to remove the event handler and return
if (foundationHostRefCompiled(target) != innerSender)
{
innerSender.PropertyChanged -= innerHandler;
return;
}
// Now we know that the inner handler is executing for an entity that still bears the correct
// relationship to target. So we just check the same way as usual - did foundation just change?
// If so, so did dependant
if (e2.PropertyName == foundationName)
invokeTarget.SafeInvoke(target, dependantName);
};
// since the foundation has shifted, the dependency will also have changed
// Raise a handler for it.
invokeTarget.SafeInvoke(sender, dependantName);
}
};
return oOut;
}
这应该做什么(它可能 - 仍然需要测试,我想我需要在这里和那里进行一些空检查)是:
- 当目标上的引用类型 属性 发生变化时,为从属 属性(由
dependantRef
标识) 引发 PropertyChanged
事件
- 此外,将
PropertyChanged
处理程序附加到 foundationHost
,这样如果基础发生变化,目标就会得到通知
- 通过在执行事件处理程序之前解析
foundationHostRef
并在发生这种情况时分离,确保目标不会收到不再与其相关的 foundationHost
的通知。
所以使用上面的逻辑,抛开嵌套超过一层的 foundationHost
s 的问题(这是一项正在进行的工作),看起来任何对象都被引用过to by foundationHostRef
将保持对 target 的闭包引用,即使它不再与它相关,至少在它尝试引发事件之前是这样。
现在我的理解是,我创建的事件处理程序可以轻松地阻止 target 占用的内存被释放。所有需要发生的事情就是某个对象在某个时候占据 foundationHostRef
,然后被重新分配到其他地方,并且比 target
具有更长的生命周期,具体取决于 target
正在做的事情从恼人的(target
是一个不占用太多内存的单例)到灾难性的(target
在程序生命周期中被创建和清空数千次,并且有一些 属性占用大量内存,GC从不回收)。
所以,我的问题是: 有什么内置保护措施(如果有的话)可以防止这种情况发生?如果没有,我应该如何调整我的 delegates/lambdas 让他们不再邪恶?
您可以通过两件事来降低这种风险:
- 如果您认为某段代码存在风险,请尽量不要在那里使用 lambda。以旧方式执行闭包:通过在 class.
上创建实例方法
- 安装 Resharper Allocation Plugin。看到常见的 C# 习语所做的所有分配是很有教育意义的。该插件会告诉您关闭了哪些变量。 R# 也会对捕获的变量发出警告 "implicitly"(不确定那是什么意思,但这听起来像是对您有用的警告)。
it looks like any object that has EVER been referred to by foundationHostRef will maintain a closure reference to target even when it is no longer related to it
我没有完全遵循上面给出的代码。从来没有无限数量的对象被闭包引用。对象引用的数量是恒定的。
不过,可能会出现问题的是,看起来未使用或超出范围的局部变量仍然会影响强引用。这是因为 C# 编译器在闭包变量未使用时不会清空它们。它也不会为普通本地人做那件事,但 JIT 足够聪明,可以做同样的事情。
在我所知道的每种情况下,显式清零您想要清除的变量都有效(尽管 C# 规范可能不保证)。
(前缀:对于那些看到这里并认为 TL;DR 的人,实际问题在最后)
自从在 C# 中发现了 lambda 和委托,我就成了它们的忠实用户。但是,在释放闭包中维护的对象上的内存时,我一直很担心,尤其是在处理嵌套闭包时。例如,考虑下面的一组 类 我正在写我认为是 "appropriate" INotifyPropertyChanged
行为的内容。
public static PropertyChangedEventHandler GetHandler<TDependant, TDependantHost, TFoundation, TFoundationHost>
(
this TDependantHost target,
PropertyChangedEventHandler invokeTarget,
Expression<Func<TDependantHost, TDependant>> dependantRef,
Expression<Func<TFoundationHost, TFoundation>> foundationRef,
Expression<Func<TDependantHost, TFoundationHost>> foundationHostRef
)
where TDependantHost : ISupportsDependencyManager
where TFoundationHost : class, INotifyPropertyChanged
{
string foundationName = GetPropertyInfo(foundationRef).Name;
string dependantName = GetPropertyInfo(dependantRef).Name;
string foundationHostName = GetPropertyInfo(foundationHostRef).Name;
Func<TDependantHost, TFoundationHost> foundationHostRefCompiled = foundationHostRef.Compile();
PropertyChangedEventHandler oOut = null;
// Complex situation. This is more complex because whilst TDependantHost bears a relationship to TFoundationHost
// the actual dependency is on a property in TFoundationHost.
// oOut is the property changed handler that will be attached to target, so it needs to
// - Raise changed events whenever foundationHostRef would evaluate to a different object
// - Whenever that change occurs, attach a new PropertyChangedEventHandler to the new foundationHost
// - ... which also handles removal of itself from target so as to guarantee
oOut = (s, e) =>
{
var sender = s as INotifyPropertyChanged;
if (sender == null)
return;
if (e.PropertyName == foundationHostName)
{
// The Foundation Host has changed. So we need to attach a new inner PropertyChangedEventHandler to it.
PropertyChangedEventHandler innerHandler = null;
innerHandler =
(s2, e2) =>
{
// Caller safety...
var innerSender = s2 as TFoundationHost;
if (innerSender == null)
return;
// Check and see if this eventhandler still points to the right object
// If it does, we'll keep going - otherwise, got to remove the event handler and return
if (foundationHostRefCompiled(target) != innerSender)
{
innerSender.PropertyChanged -= innerHandler;
return;
}
// Now we know that the inner handler is executing for an entity that still bears the correct
// relationship to target. So we just check the same way as usual - did foundation just change?
// If so, so did dependant
if (e2.PropertyName == foundationName)
invokeTarget.SafeInvoke(target, dependantName);
};
// since the foundation has shifted, the dependency will also have changed
// Raise a handler for it.
invokeTarget.SafeInvoke(sender, dependantName);
}
};
return oOut;
}
这应该做什么(它可能 - 仍然需要测试,我想我需要在这里和那里进行一些空检查)是:
- 当目标上的引用类型 属性 发生变化时,为从属 属性(由
dependantRef
标识) 引发 - 此外,将
PropertyChanged
处理程序附加到foundationHost
,这样如果基础发生变化,目标就会得到通知 - 通过在执行事件处理程序之前解析
foundationHostRef
并在发生这种情况时分离,确保目标不会收到不再与其相关的foundationHost
的通知。
PropertyChanged
事件
所以使用上面的逻辑,抛开嵌套超过一层的 foundationHost
s 的问题(这是一项正在进行的工作),看起来任何对象都被引用过to by foundationHostRef
将保持对 target 的闭包引用,即使它不再与它相关,至少在它尝试引发事件之前是这样。
现在我的理解是,我创建的事件处理程序可以轻松地阻止 target 占用的内存被释放。所有需要发生的事情就是某个对象在某个时候占据 foundationHostRef
,然后被重新分配到其他地方,并且比 target
具有更长的生命周期,具体取决于 target
正在做的事情从恼人的(target
是一个不占用太多内存的单例)到灾难性的(target
在程序生命周期中被创建和清空数千次,并且有一些 属性占用大量内存,GC从不回收)。
所以,我的问题是: 有什么内置保护措施(如果有的话)可以防止这种情况发生?如果没有,我应该如何调整我的 delegates/lambdas 让他们不再邪恶?
您可以通过两件事来降低这种风险:
- 如果您认为某段代码存在风险,请尽量不要在那里使用 lambda。以旧方式执行闭包:通过在 class. 上创建实例方法
- 安装 Resharper Allocation Plugin。看到常见的 C# 习语所做的所有分配是很有教育意义的。该插件会告诉您关闭了哪些变量。 R# 也会对捕获的变量发出警告 "implicitly"(不确定那是什么意思,但这听起来像是对您有用的警告)。
it looks like any object that has EVER been referred to by foundationHostRef will maintain a closure reference to target even when it is no longer related to it
我没有完全遵循上面给出的代码。从来没有无限数量的对象被闭包引用。对象引用的数量是恒定的。
不过,可能会出现问题的是,看起来未使用或超出范围的局部变量仍然会影响强引用。这是因为 C# 编译器在闭包变量未使用时不会清空它们。它也不会为普通本地人做那件事,但 JIT 足够聪明,可以做同样的事情。
在我所知道的每种情况下,显式清零您想要清除的变量都有效(尽管 C# 规范可能不保证)。