Search code examples
task-parallel-libraryyield-return

Dotnet yield with IEnumerable<Task<int>> not working


Playing around with yield and Task.

The following simple example runs fine.

class Program
{
    private static void Main(string[] args)
    {

        string[] messages = { "First task", "Second task", "Third task", "Fourth task" };

        var taskList = CreateTaskList(messages).ToList();

        taskList.ForEach(task => task.Start());

        Task.WaitAll(taskList.ToArray());

        Console.WriteLine("Main method complete. Press enter to finish.");
        Console.ReadLine();
    }
    static IEnumerable<Task> CreateTaskList(string[] messages)
    {
        foreach (var message in messages)
        {
            yield return new Task(obj => PrintMessage((string)obj!), message);
        }
    }

    static void PrintMessage(object message)
    {
        Console.WriteLine("Message: {0}", message);
    }
}

But the following does not. Is some deadlock at play? Its stuck at Task.WaitAll. So all that I get from Console is Before wait all

class SimpleClass
{
    public int Counter { get; set; }
}
class Program
{
    private static void Main(string[] args)
    {
        // create the simple object
        var simpleObject = new SimpleClass();

        var taskList = CreateTaskEnumerable(simpleObject, 10);

        // Start all of the tasks
        foreach (var task in taskList)
            task.Start();

        Console.WriteLine("Before wait all");

        // wait for all of the tasks to complete
        Task.WaitAll(taskList.ToArray());

        Console.WriteLine("After wait all");

        foreach (var task in taskList)
            simpleObject.Counter += task.Result;

        // write out the counter value
        Console.WriteLine("Expected value {0}, Counter value: {1}", 10000, simpleObject.Counter);
        // wait for input before exiting
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }

    private static IEnumerable<Task<int>> CreateTaskEnumerable(SimpleClass simpleObject, int numberOfTasks)
    {
        for (int i = 0; i < numberOfTasks; i++)
        {
            yield return new Task<int>((stateObject) =>
            {
                // get the state object
                var localCounter = (int)stateObject!;

                // enter a loop for 1000 increments
                for (int j = 0; j < 1000; j++)

                    // increment the counters
                    localCounter++;

                return localCounter;
            }, simpleObject.Counter);
        }
    }
}

If I remove yield altogether, the above will be as follows, and works. It gives the output as follows. I am expecting the same output from the above as well, but thats stuck. Why?

Before wait all
After wait all
Expected value 10000, Counter value: 10000
Press enter to finish

The program.

class SimpleClass
{
    public int Counter { get; set; }
}
class Program
{
    private static void Main(string[] args)
    {
        // create the bank account instance
        var simpleObject = new SimpleClass();

        // create an list of tasks
        var taskList = new List<Task<int>>();

        for (int i = 0; i < 10; i++)
        {
            // create a new task
            var task = new Task<int>((stateObject) =>
            {
                // get the state object
                var localCounter = (int)stateObject!;

                // enter a loop for 1000 increments
                for (int j = 0; j < 1000; j++)

                    // increment the counters
                    localCounter++;

                return localCounter;
            }, simpleObject.Counter);

            taskList.Add(task);
        }

        // Start all of the tasks
        foreach (var task in taskList)
            task.Start();

        Console.WriteLine("Before wait all");

        // wait for all of the tasks to complete
        Task.WaitAll(taskList.ToArray());

        Console.WriteLine("After wait all");

        foreach (var task in taskList)
            simpleObject.Counter += task.Result;
        
        // write out the counter value
        Console.WriteLine("Expected value {0}, Counter value: {1}", 10000, simpleObject.Counter);
        // wait for input before exiting
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}


Solution

  • In the first example you are calling CreateTaskList(messages).ToList() which forces CreateTaskList to yield all tasks before continuing. In the second example you do not call ToList(), and the tasks are yielded in the foreach and then started. The problem is in line Task.WaitAll(taskList.ToArray());. It takes the IEnumerable and yields the tasks again, and you are waiting for them to finish, but they are not started. In other words, every time you call foreach or ToList() on your 'yielded' IEnumerable, it will run the method CreateTaskEnumerable and create new tasks.

    One solution is to call var taskList = CreateTaskEnumerable(simpleObject, 10).ToList() or you could just manualy create list in CreateTaskEnumerable and return it.

    P.S. I would suggest you read how yield return works, or test it in https://sharplab.io/. It basically creates IEnumerable that gets its data from your method. This means your method will be executed every time your IEnumerable is enumerated.