开发者

yield returns within lock statement

if i have a yield return in a lock statement does the lock get taken out on each yield (5 times in the example below) or only once for all the items in the list?

Thanks

   开发者_如何学编程 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;
            }
        }
    }


UPDATE 2022-Oct
Reading this answer after all these years, I felt the original tone was too harsh, and I came off as an a-hole. So I soften the tone a bit.

Sorry to resurrect this from the dead, but reading the accepted answer by Daniel, and then testing it myself I though that at least those original 10 people who up-voted should be informed of the incorrect answer. Daniel has reviewed this afterwards and updated his answer to point here.

The correct answer is: The lock is NEVER released between each yeald return.
It will only be released when the enumerator is done, i.e. when the foreach loop ends.

Where Daniel's made a left turn was by scaffolding the test incorrectly. His code is not multi-threaded, and it would always compute the same way. The lock in that code is taken only once, and since it's the same thread, it's always the same lock.

I took @Daniel's code from his answer, and changed it to work with 2 threads, one for List1 and another thread created for each iteration of List2. NOTE: This is NOT how this code should be structured, for cleaner, easier to read code, see the community wiki provided by @EZI. However, this does provide a direct comparison to Daniel's code and it fleshes out the issue with the original code.

As you can see once t2 thread is started, the threads would dead-lock, since t2 is waiting on a lock that would never be released.

The Code:

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);
    }
}


Edit: This answer was wrong, but I can't delete it as it was marked as correct. Please see @Lockszmith's answer below for the correct answer.

Paraphrased:

The lock is NEVER released between each yeald return. NOTE: It is however released when the enumerator is done, i.e. when the foreach loop ends.

End Edit

Original Answer (wrong):

In your scenario, the lock will only be taken once. So in short, only once. However, you are not dealing with any shared resources. When you start to deal with shared resources like in the console app below, some interesting things happen.

You will see from the results that the lock is temporarily released on each yield. Also, note that the lock on List 1 is not released until all of the items have been written to the console, demonstrating that the GetData() method is executed partially with each iteration of the loop and that the lock must be released temporarily with each yield statement.

    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);
        }
    }
}

Results:

            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

However, he really cool thing here is the results. Note that the line, "start GetData" occurs after the call to DataGetter().GetData() but before everything that occurs within the GetData() method. This is called deferred execution and it demonstrates the beauty and usefulness of the yield return statement: Anywhere within your outer loop you can break out of the loop and there will be no more calls to the inner loop. This means that you don't have to iterate the entire inner loop if you don't have to and it also means that you will start to get results to your outer loop earlier.


@Lockszmith has a good catch (+1). I only post this since I find his code hard to read. This is a "community wiki". Feel free to update.

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;
        }
    }
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜