Различные подходы к задачам работают по-разному

Я хотел бы спросить, почему эти два подхода различны и возвращают два разных набора значений:

Первый, который, на мой взгляд, правильный, возвращает значения от 0 до 8 и работает в разных потоках (код LINQPad):

void Main()
{
    var newTasks = Enumerable.Range(0, 9).Select(x => Task.Run(() => DoSomething(x)));

    Task.WhenAll(newTasks);
}    

public int DoSomething(int value)
{
    return value;
}

Во-вторых, что, на мой взгляд, неправильно, что возвращает случайные значения специально 9, но также работает в разных потоках.

void Main()
{
    var tasks = new List<Task<int>>();
    
    for (var index = 0; index < 9; index++)
    {
        var task = Task.Run(() => DoSomething(index));
        
        tasks.Add(task);
    }
    
    Task.WaitAll(tasks.ToArray());
}    

public int DoSomething(int value)
{
    return value;
}

Можно ли изменить второй и получить результат, аналогичный первому примеру?

Спасибо за ваши ответы.


person azuremycraj    schedule 28.05.2021    source источник
comment
Связано: Захваченная переменная в цикле в C#   -  person Theodor Zoulias    schedule 28.05.2021


Ответы (1)


Можно ли изменить второй и получить результат, аналогичный первому примеру?

Когда вы передаете переменную или объект в Task.Run(()=> DoSomething(index)), вы захватываете переменную.

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

Проблема, с которой вы, скорее всего, столкнетесь, заключается в том, что CLR пытается захватить index, который является итератором, область действия которого не может выйти за его границы, если только какая-либо попытка сделать это не останется в стеке (в основном тот же поток , опять же над обобщением).

Когда CLR «захватывает» index, она захватывает только случайное значение, которое было index (из-за условий гонки), или последнее значение, которое было index, что двойное un -интуитивный, потому что вы явно сказали ему, что index никогда не должно переходить к 9 в этой строке здесь for (var index = 0; index < 9; index++).

Вот хитрая вещь: когда CLR фиксирует index, она фиксирует любое последнее значение для index. Но после завершения цикла index увеличивается еще раз, в результате чего 9 становится последним значением index.

Это может привести к целому ряду проблем, особенно если вы работаете с массивами, IndexOutOfBoundsException!

Как это исправить
На самом деле исправить очень просто! Просто создайте временную переменную в цикле и передайте ее вместо index напрямую.

Вот пример:

void Main()
{
    var tasks = new List<Task<int>>();
    
    for (var index = 0; index < 9; index++)
    {
        int tmpIndex = index;

        var task = Task.Run(() => DoSomething(tmpIndex ));
        
        tasks.Add(task);
    }
    
    Task.WaitAll(tasks.ToArray());
}

Можно ли изменить второй и получить результат, аналогичный первому примеру?

Дополнительное примечание
никогда не полагайтесь на то, что данные из задач будут в порядке, если вы явно не реализовали гарантию того, что элементы возвращаются в порядке. TaskScheduler выбирает Tasks для работы в обычном режиме FIFO (первым поступил, первым обслужен), однако, если TaskScheduler оптимизирует, расставляет приоритеты или сдвигает задачи (что происходит часто), ваши задачи не будут выполняться. выполнить по порядку, и впоследствии результаты, которые вы получите, не будут упорядочены.

person DekuDesu    schedule 28.05.2021