【问题标题】:Is yield return in C# thread-safe?C# 中的 yield return 是线程安全的吗?
【发布时间】:2010-11-25 15:24:24
【问题描述】:

我有以下代码:

private Dictionary<object, object> items = new Dictionary<object, object>;
public IEnumerable<object> Keys
{
    get
    {
        foreach (object key in items.Keys)
        {
            yield return key;
        }
    }
}

这是线程安全的吗?如果不是,我是否必须在循环周围放置 lockyield return

这就是我的意思:

Thread1 访问Keys 属性,而 Thread2 将一个项目添加到底层字典。 Thread1 是否受 Thread2 的添加影响?

【问题讨论】:

  • 您的第二个示例将锁定,返回一个可枚举然后解锁。您将在解锁后进行迭代。
  • 哦,你是对的。我只是注意到这是一个不好的例子。我将编辑问题。
  • 只是为了阐明一点:通过锁定实现线程安全是昂贵的,因此除非您明确要求,否则自动锁定每个操作是没有意义的。
  • 糟糕:我的意思是 yield-statement 不应隐式锁定,因为在所有情况下都可能不需要线程安全时,您会失去性能。

标签: c# ienumerable yield yield-return


【解决方案1】:

线程安全到底是什么意思?

您当然不应该在迭代字典时更改字典,无论是否在同一个线程中。

如果字典在多个线程中被访问,调用者应该取出一个锁(同一个覆盖所有访问),以便他们可以在迭代结果期间锁定.

编辑:为了响应您的编辑,没有它不可能对应于锁定代码。迭代器块不会自动取出锁 - 它怎么知道syncRoot

此外,仅锁定 IEnumerable&lt;TKey&gt; 的返回也不会使其成为线程安全的 - 因为锁定只影响它返回序列的时间段,而不是期间的时间段它正在被迭代。

【讨论】:

  • 是的,我指的是字典而不是列表。第一次编辑是错误的(很抱歉),我删除了它。我在问题中添加了预期的行为。
【解决方案2】:

查看这篇文章,了解使用 yield 关键字在幕后发生的事情:

Behind the scenes of the C# yield keyword

简而言之 - 编译器采用您的 yield 关键字并在 IL 中生成一个完整的类来支持该功能。您可以在跳转后查看页面并查看生成的代码......该代码看起来像跟踪线程 ID 以确保安全。

【讨论】:

    【解决方案3】:

    好的,我做了一些测试,得到了一个有趣的结果。

    似乎更多的是底层集合的枚举器问题,而不是yield关键字。枚举器(实际上是它的MoveNext 方法)抛出(如果正确实现)InvalidOperationException,因为枚举已更改。根据MSDN documentation of the MoveNext method,这是预期的行为。

    因为通过集合枚举通常不是线程安全的,yield return 也不是。

    【讨论】:

      【解决方案4】:

      我相信 yield 实现是线程安全的。实际上,您可以在家中运行这个简单的程序,您会注意到 listInt() 方法的状态已为每个线程正确保存和恢复,而不会受到其他线程的边缘影响。

      public class Test
      {
          public void Display(int index)
          {
              foreach (int i in listInt())
              {
                  Console.WriteLine("Thread {0} says: {1}", index, i);
                  Thread.Sleep(1);
              }
      
          }
      
          public IEnumerable<int> listInt()
          {
              for (int i = 0; i < 5; i++)
              {
                  yield return i;
              }
          }
      }
      
      class MainApp
      {
          static void Main()
          {
              Test test = new Test();
              for (int i = 0; i < 4; i++)
              {
                  int x = i;
                  Thread t = new Thread(p => { test.Display(x); });
                  t.Start();
              }
      
              // Wait for user
              Console.ReadKey();
          }
      }
      

      【讨论】:

      • +1。我也刚刚验证了 C# 4.0 编译器生成的 yield 迭代器状态机是线程安全的。
      【解决方案5】:

      我相信是的,但我找不到证实它的参考资料。每次任何线程在迭代器上调用 foreach 时,都应创建底层 IEnumerator 的新线程 local* 实例,因此不应存在两个线程可能发生冲突的任何“共享”内存状态......

      • 线程本地 - 从某种意义上说,它的引用变量的范围是该线程上的方法堆栈帧

      【讨论】:

      • 是的,但是如果调用 GetEnumerator 然后共享 IEnumerator 呢?他应该澄清他在用他的线程做什么。
      【解决方案6】:
      class Program
      {
          static SomeCollection _sc = new SomeCollection();
      
          static void Main(string[] args)
          {
              // Create one thread that adds entries and
              // one thread that reads them
              Thread t1 = new Thread(AddEntries);
              Thread t2 = new Thread(EnumEntries);
      
              t2.Start(_sc);
              t1.Start(_sc);
          }
      
          static void AddEntries(object state)
          {
              SomeCollection sc = (SomeCollection)state;
      
              for (int x = 0; x < 20; x++)
              {
                  Trace.WriteLine("adding");
                  sc.Add(x);
                  Trace.WriteLine("added");
                  Thread.Sleep(x * 3);
              }
          }
      
          static void EnumEntries(object state)
          {
              SomeCollection sc = (SomeCollection)state;
              for (int x = 0; x < 10; x++)
              {
                  Trace.WriteLine("Loop" + x);
                  foreach (int item in sc.AllValues)
                  {
                      Trace.Write(item + " ");
                  }
                  Thread.Sleep(30);
                  Trace.WriteLine("");
              }
          }
      }
      
      class SomeCollection
      {
          private List<int> _collection = new List<int>();
          private object _sync = new object();
      
          public void Add(int i)
          {
              lock(_sync)
              {
                  _collection.Add(i);
              }
          }
      
      
          public IEnumerable<int> AllValues
          {
              get
              {
                  lock (_sync)
                  {
                      foreach (int i in _collection)
                      {
                          yield return i;
                      }
                  }
              }
          }
      }
      

      【讨论】:

      • 这是一个简单的例子,显示 yield 本身不是线程安全的。正如所写的那样,它是线程安全的,但是如果您在 AllValues 中注释掉 lock(_sync),您应该能够通过运行几次来验证它不是线程安全的。如果你得到一个 InvalidOperationException 它证明它不是线程安全的。
      猜你喜欢
      • 1970-01-01
      • 2013-06-05
      • 2021-05-07
      • 2012-03-23
      • 1970-01-01
      • 1970-01-01
      • 2012-08-19
      • 2017-08-16
      • 2017-06-19
      相关资源
      最近更新 更多