27

如果我在 lock 语句中有一个 yield return ,是在每个 yield 上取出锁(在下面的示例中为 5 次)还是只为列表中的所有项目取出一次?

谢谢

    private List<string> _data = new List<string>(){"1","2","3","4","5"};
    private object _locker =new object();
    public IEnumerable<string> GetData()
    {
        lock (_locker)
        {
            foreach (string s in _data)
            {
                yield return s;
            }
        }
    }
4

3 回答 3

34

开始编辑
请参考@EZI 提供的社区wiki中的代码,该代码更易于阅读/更清晰。
结束编辑

很抱歉从死里复活,但阅读丹尼尔接受的答案,然后自己测试它,我认为至少那 10 个人至少应该知道这是完全错误的。

答案是:锁永远不会在每个yeald return.
注意:但是当枚举器完成时,即当 foreach 循环结束时,它会被释放。

丹尼尔的回答是错误的声称锁被多次使用。那是因为 Daniel 的代码不是多线程的,它总是以相同的方式计算。该代码中的锁只使用一次,因为它是同一个线程,所以它总是同一个锁。

我从他的回答中获取了@Daniel 的代码,并将其更改为使用 2 个线程,一个用于 List1,另一个线程用于 List2 的每次迭代。

正如你所看到的,一旦 t2 线程启动,线程就会死锁,因为 t2 正在等待一个永远不会被释放的锁。

编码:

void Main()
{
    object locker = new object();
    IEnumerable<string> myList0 = new DataGetter().GetData(locker, "List 0");
    IEnumerable<string> myList1 = new DataGetter().GetData(locker, "List 1");
    IEnumerable<string> myList2 = new DataGetter().GetData(locker, "List 2");

    Console.WriteLine("start Getdata");
    // Demonstrate that breaking out of a foreach loop releasees the lock
    var t0 = new Thread(() => {
        foreach( var s0 in myList0 )
        {
            Console.WriteLine("List 0 {0}", s0);
            if( s0 == "2" ) break;
        }
    });
    Console.WriteLine("start t0");
    t0.Start();
    t0.Join(); // Acts as 'wait for the thread to complete'
    Console.WriteLine("end t0");

    // t1's foreach loop will start (meaning previous t0's lock was cleared
    var t1 = new Thread(() => {
        foreach( var s1 in myList1)
        {
            Console.WriteLine("List 1 {0}", s1);
            // Once another thread will wait on the lock while t1's foreach
            // loop is still active a dead-lock will occure.
            var t2 = new Thread(() => {
                foreach( var s2 in myList2 )
                {
                    Console.WriteLine("List 2 {0}", s2);
                }
            } );
            Console.WriteLine("start t2");          
            t2.Start();
            t2.Join();
            Console.WriteLine("end t2");            
        }
    });
    Console.WriteLine("start t1");
    t1.Start();
    t1.Join();
    Console.WriteLine("end t1");
    Console.WriteLine("end GetData");
}

void foreachAction<T>( IEnumerable<T> target, Action<T> action )
{
    foreach( var t in target )
    {
        action(t);
    }
}

public class DataGetter
{
    private List<string> _data = new List<string>() { "1", "2", "3", "4", "5" };

    public IEnumerable<string> GetData(object lockObj, string listName)
    {
        Console.WriteLine("{0} Starts", listName);
        lock (lockObj)
        {
            Console.WriteLine("{0} Lock Taken", listName);
            foreach (string s in _data)
            {
                yield return s;
            }
        }
        Console.WriteLine("{0} Lock Released", listName);
    }
}
于 2013-04-03T16:29:55.537 回答
9

编辑: 这个答案是错误的,但我不能删除它,因为它被标记为正确。请参阅下面@Lockszmith 的答案以获取正确答案。

释义:

在每次 yeald 返回之间永远不会释放锁。注意:但是当枚举器完成时,即当 foreach 循环结束时,它会被释放。

结束编辑

原始答案(错误):

在您的场景中,锁只会被占用一次。所以简而言之,只有一次。但是,您没有处理任何共享资源。当您开始像下面的控制台应用程序那样处理共享资源时,会发生一些有趣的事情。

你会从结果中看到,每次yield都会临时释放锁。另外,请注意,在所有项目都写入控制台之前,不会释放列表 1 上的锁,这表明 GetData() 方法在循环的每次迭代中部分执行,并且必须在每次循环时临时释放锁产量声明。

    static void Main(string[] args)
    {
        object locker = new object();
        IEnumerable<string> myList1 = new DataGetter().GetData(locker, "List 1");
        IEnumerable<string> myList2 = new DataGetter().GetData(locker, "List 2");
        Console.WriteLine("start Getdata");
        foreach (var x in myList1)
        {
            Console.WriteLine("List 1 {0}", x);
            foreach(var y in myList2)
            {
                Console.WriteLine("List 2 {0}", y);
            }
        }
        Console.WriteLine("end GetData");
        Console.ReadLine();
    }

    public class DataGetter
    {
        private List<string> _data = new List<string>() { "1", "2", "3", "4", "5" };

        public IEnumerable<string> GetData(object lockObj, string listName)
        {
            Console.WriteLine("{0} Starts", listName);
            lock (lockObj)
            {
                Console.WriteLine("{0} Lock Taken", listName);
                foreach (string s in _data)
                {
                    yield return s;
                }
            }
            Console.WriteLine("{0} Lock Released", listName);
        }
    }
}

结果:

            start Getdata
            List 1 Starts
            List 1 Lock Taken
            List 1 1
            List 2 Starts
            List 2 Lock Taken
            List 2 1
            List 2 2
            List 2 3
            List 2 4
            List 2 5
            List 2 Lock Released
            List 1 2
            List 2 Starts
            List 2 Lock Taken
            List 2 1
            List 2 2
            List 2 3
            List 2 4
            List 2 5
            List 2 Lock Released
            List 1 3
            List 2 Starts
            List 2 Lock Taken
            List 2 1
            List 2 2
            List 2 3
            List 2 4
            List 2 5
            List 2 Lock Released
            List 1 4
            List 2 Starts
            List 2 Lock Taken
            List 2 1
            List 2 2
            List 2 3
            List 2 4
            List 2 5
            List 2 Lock Released
            List 1 5
            List 2 Starts
            List 2 Lock Taken
            List 2 1
            List 2 2
            List 2 3
            List 2 4
            List 2 5
            List 2 Lock Released
            List 1 Lock Released
            end GetData

不过,他这里真正酷的事情是结果。请注意,“开始 GetData”这一行出现在调用 DataGetter().GetData() 之后,但在 GetData() 方法中发生的所有事情之前。这称为延迟执行,它展示了 yield return 语句的美丽和实用性:在外部循环中的任何地方,您都可以跳出循环,并且不会再调用内部循环。这意味着您不必迭代整个内部循环,如果您不需要,这也意味着您将开始更早地获得外部循环的结果。

于 2010-05-17T08:46:44.467 回答
5

@Lockszmith 有一个很好的收获(+1)。我只发布这个,因为我发现他的代码很难阅读。这是一个“社区维基”。随时更新。

object lockObj = new object();

Task.Factory.StartNew((_) =>
{
    System.Diagnostics.Debug.WriteLine("Task1 started");
    var l1 = GetData(lockObj, new[] { 1, 2, 3, 4, 5, 6, 7, 8 }).ToList();
}, TaskContinuationOptions.LongRunning);

Task.Factory.StartNew((_) =>
{
    System.Diagnostics.Debug.WriteLine("Task2 started");
    var l2 = GetData(lockObj, new[] { 10, 20, 30, 40, 50, 60, 70, 80 }).ToList();
}, TaskContinuationOptions.LongRunning);

public IEnumerable<T> GetData<T>(object lockObj, IEnumerable<T> list)
{
    lock (lockObj)
    {
        foreach (T x in list)
        {
            System.Diagnostics.Debug.WriteLine("Thread " + Thread.CurrentThread.ManagedThreadId + " returned "  + x );
            Thread.Sleep(1000);
            yield return x;
        }
    }
}
于 2015-03-13T22:49:03.927 回答