Цикл for выходит за границы списка

Не могу понять баг или фитча. В общем я в цикле For запускаю таски и если задержка Thread.Sleep(1) очень мала или отсутствует совсем то выкидывает исключения выхода за границы массива и значение индексатора i на самом деле выше условленного значения (условие: i строго меньше количества элементов), а в процессе работы i равна количеству элементов, получается что условие не соблюдается. Что за Хэ.

Если задержку Thread.Sleep(1) увеличить то такой истории не случается

private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                var input = textBox1.Text.Split(' ');
                var freq = new List<int>();
                var durations = new List<int>();
                int counter = 1;
                foreach (var el in input)
                {
                    if (counter % 2 != 0)
                    {
                        freq.Add(int.Parse(el));
                    }
                    else durations.Add(int.Parse(el));
                    counter++;
                }
                // Этот цикл
                for (int i = 0; i < freq.Count; i++)
                {
                    Task.Factory.StartNew(() =>
                    {
                        Console.Beep(freq[i], durations[i]);
                    });
                    Thread.Sleep(1);
                }
                // Этот цикл
            }
            catch
            {
                textBox1.Clear();
            }
        }

Что я упускаю? Может есть у кого какие мысли.


Ответы (2 шт):

Автор решения: AlexanderSt

Замените на

Task.Factory.StartNew((object _par) =>
{
    int j = (int)_par;
    Console.Beep(freq[j], durations[j]);
}, i);

и всё прекрасно отработает

→ Ссылка
Автор решения: CrazyElf

Давайте посмотрим, что происходит в самом конце вот этого цикла:

for (int i = 0; i < freq.Count; i++)
{
    ...
}

Переменная i (она была равна freq.Count - 1) в последний раз увеличивается на 1 и становится равной freq.Count, после этого проверяется условие входа в тело цикла i < freq.Count, оно уже не выполняется и в тело цикла управление не попадает. Но! Вот эта таска стартовала ещё на предыдущей итерации цикла, получив ссылку на переменную i ("захватив" переменную i) и ещё не успела толком начать исполняться, когда прошла эта самая последняя итерация цикла (без захода в тело цикла):

// Тут i = freq.Count - 1
Task.Factory.StartNew(() =>
{
    // А когда задача стала исполняться параллельно циклу, 
    // то уже пошла следующая итерация цикла и i = freq.Count
    Console.Beep(freq[i], durations[i]);
});

Чтобы избежать такого эффекта, нужно передавать переменную i как параметр либо лямбда-вызова, либо как параметр обычного метода типа Task и тогда этой проблемы не будет. "Захват переменной" произошёл потому, что она не была передана как параметр метода, а была использована в анонимной функции как есть, то есть, в итоге, по ссылке.

P.S. Почему же ситуацию исправляет увеличение задержки в Thread.Sleep;? Потому, что тогда следующая итерация цикла не успевает начаться до того, как стартует и выполнится таска. Таске нужно некоторое время на то, чтобы стартовать, ну и чтобы выполниться тоже. И достаточная величина паузы перед следующей итерацией цикла даёт ей это время.

P.P.S. То есть основной момент тут именно то, что переменная цикла всё-таки может "выходить за границу", но в обычных условиях мы это не можем наблюдать, потому что внутрь тела цикла управление при этом не попадает, а после окончания кода цикла переменная уже не актуальна и не видима, её область действия ограничена циклом. Но вот благодаря "захвату переменной" и запускаемой в цикле задаче, которая исполняется параллельно, мы всё-таки можем увидеть переменную цикла, вышедшую за заданный ей диапазон итерации.

→ Ссылка