Создает ли tasks.ToList() список, содержащий недавно скопированные задачи, или список относится к тем же задачам?

Предположим, что у нас есть массив задач (называемых "задачами" ), а затем преобразовываем его в список (называемый "temp" ), говоря:

var temp = tasks.ToList();

Что происходит с теми запущенными задачами, на которые указывают элементы массива? У нас есть два набора задач, выполняемых отдельно (один в "задачах", а другой - в "temp" )? Или они указывают на одни и те же задачи?

Следующий код (взятый из книги Exam 70-483) относится к тому, что я говорю (последние три строки):

Task<int>[] tasks = new Task<int>[3];

tasks[0] = Task.Run(() => { Thread.Sleep(2000); return 1; });
tasks[1] = Task.Run(() => { Thread.Sleep(1000); return 2; });
tasks[2] = Task.Run(() => { Thread.Sleep(3000); return 3; });

while (tasks.Length > 0) {
    int i = Task.WaitAny(tasks);
    Task<int> completedTask = tasks[i];

    Console.WriteLine(completedTask.Result);

    var temp = tasks.ToList();
    temp.RemoveAt(i);
    tasks = temp.ToArray();
}

ОБНОВЛЕНИЕ: Я знаю цель последних трех строк, но не знаю, почему это работает.

Ответ 1

Что происходит [когда мы вызываем ToList на последовательность задач]? У нас есть два набора задач, выполняемых отдельно? Или они указывают на одни и те же задачи?

Это действительно хороший вопрос, и это зависит от исходной последовательности, на которую вы вызываете ToList. Если temp - это набор запущенных задач (как в вашем случае), то ToList просто создает копию своих ссылок, что означает, что ваш новый список будет указывать на один и тот же набор задач.

Однако, если temp представляет собой последовательность задач, которые еще не созданы (из-за отложенного выполнения), тогда вы будете получать новый набор задач каждый раз, когда вы вызываете ToList. Вот простой пример (используя ToArray, который имеет тот же эффект, что и ToList):

int count = 0;

var tasks = Enumerable.Range(0, 10).Select(_ => 
    Task.Run(() => Interlocked.Increment(ref count)));

// tasks = tasks.ToArray();   // deferred vs. eager execution

Task.WaitAll(tasks.ToArray());
Task.WaitAll(tasks.ToArray());

Console.WriteLine(count);

Если вы запустите код выше, конечный результат будет 20, а это означает, что были запущены две партии задач (по одному для каждого вызова ToArray). Тем не менее, если вы раскомментируете строку, которая позволяет выполнить более энергичное выполнение, конечный результат станет 10, указывая, что последующие вызовы ToArray копируют ссылки только вместо новых заданий.

Ответ 2

ToList создает новый список, содержащий элементы IEnumerable. Он не создает глубокую копию этих элементов, он только копирует ссылки. Таким образом, существует только набор одиночных задач.

Эти строки, вероятно, существуют, поэтому разработчик может использовать RemoveAt для простого удаления элемента с помощью индекса (индекс, возвращаемый из Task.WaitAny)