The*_*ias 10 .net c# garbage-collection iterator memory-leaks
我有一个生成 DNA 序列的类,这些序列由长字符串表示。该类实现了该IEnumerable<string>接口,并且它可以产生无限数量的DNA序列。下面是我的课程的简化版本:
class DnaGenerator : IEnumerable<string>
{
private readonly IEnumerable<string> _enumerable;
public DnaGenerator() => _enumerable = Iterator();
private IEnumerable<string> Iterator()
{
while (true)
foreach (char c in new char[] { 'A', 'C', 'G', 'T' })
yield return new String(c, 10_000_000);
}
public IEnumerator<string> GetEnumerator() => _enumerable.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Run Code Online (Sandbox Code Playgroud)
此类使用迭代器生成 DNA 序列。无需一次又一次地调用迭代器,而是IEnumerable<string>在构造期间创建一个实例并将其缓存为私有字段。问题在于,使用此类会导致不断分配相当大的内存块,而垃圾收集器无法回收该块。以下是此行为的最小演示:
var dnaGenerator = new DnaGenerator();
Console.WriteLine($"TotalMemory: {GC.GetTotalMemory(true):#,0} bytes");
DoWork(dnaGenerator);
GC.Collect();
Console.WriteLine($"TotalMemory: {GC.GetTotalMemory(true):#,0} bytes");
GC.KeepAlive(dnaGenerator);
static void DoWork(DnaGenerator dnaGenerator)
{
foreach (string dna in dnaGenerator.Take(5))
{
Console.WriteLine($"Processing DNA of {dna.Length:#,0} nucleotides" +
$", starting from {dna[0]}");
}
}
Run Code Online (Sandbox Code Playgroud)
输出:
class DnaGenerator : IEnumerable<string>
{
private readonly IEnumerable<string> _enumerable;
public DnaGenerator() => _enumerable = Iterator();
private IEnumerable<string> Iterator()
{
while (true)
foreach (char c in new char[] { 'A', 'C', 'G', 'T' })
yield return new String(c, 10_000_000);
}
public IEnumerator<string> GetEnumerator() => _enumerable.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Run Code Online (Sandbox Code Playgroud)
我的期望是所有生成的 DNA 序列都适合垃圾回收,因为它们没有被我的程序引用。我持有的唯一引用是对实例本身的引用DnaGenerator,它并不意味着包含任何序列。该组件仅生成序列。然而,无论我的程序生成多少序列,在完全垃圾回收后总会分配大约 20 MB 的内存。
我的问题是:为什么会发生这种情况?我怎样才能防止这种情况发生?
.NET 6.0、Windows 10、64 位操作系统、基于 x64 的处理器、发布版本。
更新:如果我替换它,问题就会消失:
public IEnumerator<string> GetEnumerator() => _enumerable.GetEnumerator();
Run Code Online (Sandbox Code Playgroud)
...有了这个:
public IEnumerator<string> GetEnumerator() => Iterator().GetEnumerator();
Run Code Online (Sandbox Code Playgroud)
但我不喜欢每次需要枚举器时创建一个新的枚举。我的理解是一个IEnumerable<T>可以创建多个IEnumerator<T>s。AFAIK 这两个接口并不意味着具有一对一的关系。
该问题是由使用自动生成的代码实现引起的yield。
您可以通过显式实现枚举器来稍微缓解这种情况。
.Reset()您必须通过调用from来稍微修改它public IEnumerator<string> GetEnumerator(),以确保枚举在每次调用时重新启动:
class DnaGenerator : IEnumerable<string>
{
private readonly IEnumerator<string> _enumerable;
public DnaGenerator() => _enumerable = new IteratorImpl();
sealed class IteratorImpl : IEnumerator<string>
{
public bool MoveNext()
{
return true; // Infinite sequence.
}
public void Reset()
{
_index = 0;
}
public string Current
{
get
{
var result = new String(_data[_index], 10_000_000);
if (++_index >= _data.Length)
_index = 0;
return result;
}
}
public void Dispose()
{
// Nothing to do.
}
readonly char[] _data = { 'A', 'C', 'G', 'T' };
int _index;
object IEnumerator.Current => Current;
}
public IEnumerator<string> GetEnumerator()
{
_enumerable.Reset();
return _enumerable;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Run Code Online (Sandbox Code Playgroud)
请注意,10_000_000 个字符(16 位)将占用大约 20 MB。如果您看一下,decompilation您会注意到生成的yeild return内部<Iterator>类的结果,该内部类又具有一个用于current存储字符串的字段(以实现IEnumerator<string>.Current):
[CompilerGenerated]\nprivate sealed class <Iterator>d__2 : IEnumerable<string>, IEnumerable, IEnumerator<string>, IEnumerator, IDisposable\n{\n\xe2\x80\x8b ...\n private string <>2__current;\n ...\n}\nRun Code Online (Sandbox Code Playgroud)\n并且Iterator方法内部将被编译为如下所示:
[IteratorStateMachine(typeof(<Iterator>d__2))]\nprivate IEnumerable<string> Iterator()\n{\n return new <Iterator>d__2(-2);\n}\nRun Code Online (Sandbox Code Playgroud)\n这导致当前字符串始终存储在内存中以供_enumerable.GetEnumerator();实现(迭代开始后),而DnaGenerator实例本身并未被GC。
UPD
\n\n\n我的理解是,单个 IEnumerable 可以创建许多 IEnumerator。AFAIK 这两个接口并不意味着具有一对一的关系。
\n
是的,在为可枚举生成的情况下,yield return它可以创建多个枚举器,但在这种特殊情况下,实现具有“一对一”关系,因为生成的实现是IEnumerable和IEnumerator:
[IteratorStateMachine(typeof(<Iterator>d__2))]\nprivate IEnumerable<string> Iterator()\n{\n return new <Iterator>d__2(-2);\n}\nRun Code Online (Sandbox Code Playgroud)\n\n\n但我不喜欢每次需要枚举器时创建一个新的枚举。
\n
但这实际上是你调用时发生的事情_enumerable.GetEnumerator()(这显然是一个实现细节),如果你检查已经提到的反编译,你会发现它_enumerable = Iterator()实际上是new <Iterator>d__2(-2)这样<Iterator>d__2.GetEnumerator()的:
IEnumerator<string> IEnumerable<string>.GetEnumerator()\n{\n if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)\n {\n <>1__state = 0;\n return this;\n }\n return new <Iterator>d__2(0);\n}\nRun Code Online (Sandbox Code Playgroud)\n因此,除了第一个枚举之外,它实际上应该每次都创建一个新的迭代器实例,所以你的public IEnumerator<string> GetEnumerator() => Iterator().GetEnumerator();方法就很好。