Странная обработка потоков на C#

#c# #multithreading

#c# #многопоточность

Вопрос:

Я столкнулся со странной проблемой с обработкой потоков C #.

Это мой пример программы, использующей thread для «активации» функции Print () у каждого агента в списке агентов.

 class Program {

    static void Main(string[] args) {

        List<Agent> agentList = new List<Agent>();

        agentList.Add(new Agent("lion"));
        agentList.Add(new Agent("cat"));
        agentList.Add(new Agent("dog"));
        agentList.Add(new Agent("bird"));

        foreach (var agent in agentList) {
            new Thread(() => agent.Print()).Start();
        }

        Console.ReadLine();
    }
}

class Agent {
    public string Name { get; set; }

    public Agent(string name) {
        this.Name = name;
    }

    public void Print() {
        Console.WriteLine("Agent {0} is called", this.Name);
    }
}
  

И вот результат, когда я запускаю вышеупомянутую программу:

 Agent cat is called
Agent dog is called
Agent bird is called
Agent bird is called
  

Но я ожидал, что что-то содержит все 4 агента, такие как

 Agent lion is called
Agent cat is called
Agent dog is called
Agent bird is called
  

Самое удивительное, что если я вызывал потоки вне foreach, это работало!

 class Program {
    static void Main(string[] args) {
        List<Agent> agentList = new List<Agent>();

        agentList.Add(new Agent("leecom"));
        agentList.Add(new Agent("huanlv"));
        agentList.Add(new Agent("peter"));
        agentList.Add(new Agent("steve"));

        new Thread(() => agentList[0].Print()).Start();
        new Thread(() => agentList[1].Print()).Start();
        new Thread(() => agentList[2].Print()).Start();
        new Thread(() => agentList[3].Print()).Start();


        Console.ReadLine();
    }
}
  

Результат приведенного выше кода — именно то, что я ожидал. Так в чем же здесь проблема?

Комментарии:

1. Вот что происходит, когда вы закрываете переменную цикла: blogs.msdn.com/b/ericlippert/archive/2009/11/12 /…

2. @dlev, ты должен опубликовать этот ответ, чтобы он мог отметить, что проблема решена. Ваш ответ правильный.

3. Вы знаете, если вы используете .Net 4.0, вы можете использовать Parallel для каждого цикла, чтобы делать то, что вы хотите. Я бы подумал, что это было бы более уместно.

Ответ №1:

Что у вас там есть, так это замыкание. Вы закрываете переменную внутри цикла foreach. Происходит то, что переменная перезаписывается перед запуском вашего потока, поэтому у вас есть две итерации с одинаковым значением.

Простое решение заключается в том, чтобы зафиксировать значение внутри цикла foreach перед его использованием:

 foreach(var a in agentList)
{
    var agent = a;
    new Thread(() => agent.Print()).Start();
}
  

Комментарии:

1. Обратите внимание, что Resharper от Jetbrains уведомил бы вас о такой проблеме. (Я не связан с Jetbrains в какой-либо форме … просто люблю Resharper!)

Ответ №2:

Вы выполняете захват agent в замыкании, что может быть проблематично при многопоточности. Сначала назначьте локальной переменной:

     foreach (var agent in agentList) {
        var temp = agent;
        new Thread(() => temp.Print()).Start();
    }
  

Комментарии:

1. временные параметры и агент будут иметь одинаковое отношение. таким образом, будет вероятность такого же неправильного вывода. будет работать для примитивных наборов данных, а не для пользовательских.

2. @user998660 — область действия temp ограничена внутри foreach и будет иметь исходную ссылку только после того, как агент будет изменен на следующей итерации цикла.

3. @Justin: Да, правильно. Это локальная ссылка, которая необходима, чтобы сохранить саму ссылку от изменения при закрытии.

Ответ №3:

Измените свой цикл foreach следующим образом:

 foreach (var agent in agentList) 
{
    var agent1 = agent;
    new Thread(() => agent1.Print()).Start();         
} 
  

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

Ответ №4:

Это из-за агента var, который может иметь такое же отношение к потокам in. Вот почему ваш подход eariler не рекомендуется.

Ответ №5:

Без использования какой-либо синхронизации потоков (например, ManualResetEvent) вы не можете гарантировать порядок обработки потоков. Если вы хотите выполнить несколько шагов по порядку, я бы посоветовал вам объединить работу, а затем выполнить все это в одном фоновом потоке.

Мне нравится объект BackgroundWorker:

 List<Agent> agentList = new List<Agent>();

agentList.Add(new Agent("leecom"));
agentList.Add(new Agent("huanlv"));
agentList.Add(new Agent("peter"));
agentList.Add(new Agent("steve"));

BackgroundWorker worker = new BackgroundWorker();
worker.DoWork  = (sender, e) =>
{
    foreach (var item in agentList)
    {
        item.Print();
    }
};

worker.RunWorkerAsync();
  

Комментарии:

1. О да, другие ответы верны, это закрытие. Я бы все равно использовал свой код, но это мой вкус. : D

2. Ваш код принципиально отличается. Он будет использовать только одного фонового рабочего, тогда как OP хочет отдельного рабочего для каждого агента. Вам все равно понадобилось бы исправить замыкание в вашем коде.

3. OP никогда явно не заявлял, что они хотят, чтобы каждая печать вызывалась в отдельных потоках. Часто разработчики создают отдельные потоки, когда достаточно одного потока. Кроме того, поскольку цикл происходит внутри нового потока, закрытие переменной не является проблемой так же, как в исходном примере кода, при условии, что они не изменяют список агентов после вызова RunWorkingAsync, мой пример кода будет работать правильно.