Неожиданное поведение для ThreadPool.QueueUserWorkItem - PullRequest
6 голосов
/ 20 января 2011

Пожалуйста, проверьте пример кода ниже:

public class Sample
{
    public int counter { get; set; }
    public string ID;
    public void RunCount()
    {
        for (int i = 0; i < counter; i++)
        {
            Thread.Sleep(1000);

            Console.WriteLine(this.ID + " : " + i.ToString());
        }
    }
}

class Test
{
    static void Main()
    {
        Sample[] arrSample = new Sample[4];

        for (int i = 0; i < arrSample.Length; i++)
        {
            arrSample[i] = new Sample();
            arrSample[i].ID = "Sample-" + i.ToString();
            arrSample[i].counter = 10;
        }

        foreach (Sample s in arrSample)
        {
            ThreadPool.QueueUserWorkItem(callback => s.RunCount());
        }

        Console.ReadKey();
    }

}

Ожидаемый результат для этого образца должен быть примерно таким:

Sample-0 : 0 
Sample-1 : 0 
Sample-2 : 0 
Sample-3 : 0 
Sample-0 : 1 
Sample-1 : 1 
Sample-2 : 1 
Sample-3 : 1
.
. 
.

Однако, когда вы запустите этот код, он покажет что-то вроде этого:

Sample-3 : 0 
Sample-3 : 0 
Sample-3 : 0 
Sample-3 : 1 
Sample-3 : 1 
Sample-3 : 0 
Sample-3 : 2 
Sample-3 : 2
Sample-3 : 1 
Sample-3 : 1
.
. 
.

Я могу понять, что порядок, в котором выполняются потоки, может отличаться, и, следовательно, число не увеличивается в циклическом порядке. Однако я не понимаю, почему все ID отображаются как Sample-3, в то время как выполнение явно происходит независимо друг от друга.

Не используются ли разные объекты с разными потоками?

1 Ответ

10 голосов
/ 20 января 2011

Это старая модифицированная проблема закрытия.Возможно, вы захотите посмотреть: Threadpools - возможная проблема порядка выполнения потоков для аналогичного вопроса и сообщение в блоге Эрика Липперта Закрытие переменной цикла, которая считается вредной , для понимания проблемы.

По сути, лямбда-выражение, которое вы там получаете, захватывает переменную s, а не значение переменной в точке, где объявлена ​​лямбда.Следовательно, последующие изменения, внесенные в значение переменной , являются видимыми для делегата.Экземпляр Sample, в котором будет выполняться метод RunCount, будет зависеть от экземпляра , на который ссылается переменная s (ее значение) в точке, в которой делегат фактически выполняет .

Кроме того, поскольку делегаты (компилятор фактически использует один и тот же экземпляр делегата) выполняются асинхронно, не гарантируется, что эти значения будут находиться в точке каждого выполнения.В настоящее время вы видите, что цикл foreach завершается в главном потоке до любого вызова делегата (как и следовало ожидать - требуется время для планирования задач в пуле потоков).Таким образом, все рабочие элементы в конечном итоге увидят «окончательное» значение переменной цикла.Но это не гарантируется никакими средствами;попробуйте вставить разумную длительность Thread.Sleep внутри цикла, и вы увидите другой вывод.


Обычное исправление:

  1. Введите другую переменную внутри тело цикла.
  2. Назначьте эту переменную текущему значению переменной цикла.
  3. Захват переменной «copy» вместо переменной цикла внутри лямбды.

    foreach (Sample s in arrSample)
    {
        Sample sCopy = s;
        ThreadPool.QueueUserWorkItem(callback => sCopy.RunCount());
    }
    

Теперь каждый рабочий элемент «владеет» определеннымзначение переменной цикла.


Другой вариант в этом случае - полностью избежать проблемы, не захватывая ничего:

ThreadPool.QueueUserWorkItem(obj => ((Sample)obj).RunCount(), s);
...