Цикл 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 шт):
Замените на
Task.Factory.StartNew((object _par) =>
{
int j = (int)_par;
Console.Beep(freq[j], durations[j]);
}, i);
и всё прекрасно отработает
Давайте посмотрим, что происходит в самом конце вот этого цикла:
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. То есть основной момент тут именно то, что переменная цикла всё-таки может "выходить за границу", но в обычных условиях мы это не можем наблюдать, потому что внутрь тела цикла управление при этом не попадает, а после окончания кода цикла переменная уже не актуальна и не видима, её область действия ограничена циклом. Но вот благодаря "захвату переменной" и запускаемой в цикле задаче, которая исполняется параллельно, мы всё-таки можем увидеть переменную цикла, вышедшую за заданный ей диапазон итерации.