【问题标题】:Why does Lazy<T> using ExecutionAndPublication behave differently with yielded IEnumerables?为什么使用 ExecutionAndPublication 的 Lazy<T> 与产生的 IEnumerables 的行为不同?
【发布时间】:2017-07-26 17:42:57
【问题描述】:

我的代码中存在一个问题,即调用延迟初始化程序的频率超出了我的预期。从文档中,我预计使用 LazyThreadSafetyMode.ExecutionAndPublication 将确保我的初始化函数只被调用一次,例如,如果在定义后访问 numbers.Value:

numbers = new Lazy<IEnumerable<int>>(
        () => GetNumbers(),
        LazyThreadSafetyMode.ExecutionAndPublication
    );

但是,我发现如果初始化函数产生结果,初始化函数会被多次调用。我认为这与延迟执行有关,但我只有模糊的感觉。

问题:

在下面的代码中,为什么各个初始化函数的执行次数不同?

void Main()
{
    var foo         = new foo();
    var tasks       = new List<Task>();

    for (int i = 0; i < 10; ++i) tasks.Add(Task.Run(() => {foreach (var number in foo.Numbers) Debug.WriteLine(number);})); 
    Task.WaitAll(tasks.ToArray());
    tasks.Clear();
    for (int i = 0; i < 10; ++i) tasks.Add(Task.Run(() => {foreach (var letter in foo.Letters) Debug.WriteLine(letter);})); 
    Task.WaitAll(tasks.ToArray());
}

public class foo
{
    public IEnumerable<int> Numbers => numbers.Value;
    public IEnumerable<char> Letters => letters.Value;
    readonly Lazy<IEnumerable<int>> numbers;
    readonly Lazy<IEnumerable<char>> letters;

    public foo()
    {
        numbers = new Lazy<IEnumerable<int>>(
            () => GetNumbers(),
            LazyThreadSafetyMode.ExecutionAndPublication
        );
        letters = new Lazy<IEnumerable<char>>(
            () => GetLetters().ToList(), //ToList enumerates all yielded letters, creating the expected call once behavior
            LazyThreadSafetyMode.ExecutionAndPublication
        );
    }

    protected IEnumerable<char> GetLetters()
    {
        Debug.WriteLine($"{nameof(GetLetters)} Called");
        yield return 'a';
        yield return 'b';
        yield return 'c';
        yield break;
    }
    protected IEnumerable<int> GetNumbers()
    {
        Debug.WriteLine($"{nameof(GetNumbers)} Called");
        yield return 1;
        yield return 2;
        yield return 3;
        yield break;
    }
}

【问题讨论】:

  • 您需要了解yield 的工作原理。基本上你的方法变成了状态机。每次你迭代它时,你的代码都会被调用。这实际上与Lazy&lt;T&gt; 无关,正如@dcg 指出的那样,使用yield 已经很懒惰,因此无需将其包装在Lazy&lt;T&gt; 对象中。如果您想将方法的结果缓存在内存中,您可以使用ToList,然后Lazy&lt;T&gt; 可用于延迟ToList 调用迭代该方法并将结果放入内存。

标签: c# ienumerable lazy-initialization


【解决方案1】:

我的代码中有一个问题,即延迟初始化程序的调用频率比我预期的要高。

不,lazy 的初始化器被调用一次。懒惰的初始化器是

() => GetNumbers()

而且只调用一次。

GetNumbers 返回一个IEnumerable&lt;int&gt;——一个整数序列。

当你foreach那个序列时,它调用GetEnumerator来获取一个枚举器,然后在枚举器对象上调用MoveNext直到MoveNext返回false

您说过您希望将序列枚举为:

the first time MoveNext is called, do a writeline and produce 1
the second time MoveNext is called, produce 2
the third time MoveNext is called, produce 3
the fourth time MoveNext is called, produce 4
Every subsequent time MoveNext is called, return false

所以每次枚举序列时,都会发生这种情况。

你能解释一下你预期会发生什么吗?我有兴趣了解为什么人们对计算机程序抱有错误的信念。

我也不清楚你为什么使用Lazy。您通常会使用Lazy 来避免在需要之前进行昂贵的工作,但序列已经推迟工作直到它们被枚举。

【讨论】:

  • OP 表示惊讶地发现两个 sn-ps 提供的行为不同,所以我认为可以公平地说他们期望第一个序列的行为方式与第二个序列的行为方式相同。
  • @Servy:说白了。我希望能更详细地了解证明这种信念的“心理模型”。
  • @EricLippert:我会注意到,当我第一次遇到Lazy 时,我的心智模型是“如果你想初始化一次就使用这个。初始化将被推迟。”因此,如果 A) 我需要一个静态的东西来初始化一次,B) 我不介意初始化是否被推迟,并且 C) 我担心竞争条件,我会使用 Lazy。 C 并不是心智模型的一部分。它在那里扰乱我的心理工具箱,以便更好的解决方案不会坐在顶部。现在Lazy 在我的心智模型中被标记为“谨慎使用。以前的心智模型会导致错误的决定。”
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-08-12
  • 2021-11-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-25
相关资源
最近更新 更多