Разве это не должно потерпеть неудачу без использования блокировки? Простой производитель-потребитель

У меня есть очередь, список с потоками производителей и список с потоками потребителей.

Мой код выглядит так

    public class Runner
{
    List<Thread> Producers;
    List<Thread> Consumers;
    Queue<int> queue;
    Random random;

    public Runner()
    {
        Producers = new List<Thread>();
        Consumers = new List<Thread>();

        for (int i = 0; i < 2; i++)
        {
            Thread thread = new Thread(Produce);
            Producers.Add(thread);
        }

        for (int i = 0; i < 2; i++)
        {
            Thread thread = new Thread(Consume);
            Consumers.Add(thread);
        }

        queue = new Queue<int>();
        random = new Random();

        Producers.ForEach(( thread ) => { thread.Start(); });
        Consumers.ForEach(( thread ) => { thread.Start(); });
    }

    protected void Produce()
    {
        while (true)
        {
                int number = random.Next(0, 99);
                queue.Enqueue(number);
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " Produce: " + number);
        }
    }

    protected void Consume()
    {
        while (true)
        {
                if (queue.Any())
                {
                    int number = queue.Dequeue();
                    Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " Consume: " + number);
                }
                else
                {
                    Console.WriteLine("No items to consume");
                }
        }
    }
}

Разве это не должно привести к неудаче из-за отсутствия использования ключевого слова блокировки? Однажды он потерпел неудачу, потому что пытался удалить очередь, когда очередь была пустой, использование ключевого слова блокировки исправит это, верно?

Если ключевое слово блокировки не требуется для приведенного выше кода, то когда оно необходимо?

Заранее спасибо! знак равно


person Moulde    schedule 01.06.2011    source источник


Ответы (6)


Блокировка предназначена для устранения аномального поведения приложения, особенно в многопоточности. Наиболее распространенной целью является устранение «состояния гонки», которое вызывает недетерминированное поведение программы.

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

person Chris Marisic    schedule 01.06.2011

Использование Queue без блокировок действительно не является потокобезопасным. Но лучше, чем использовать блокировки, вы можете попробовать ConcurrentQueue. Google для «C # ConcurrentQueue», и вы найдете довольно много примеров, например. этот сравнивает использование и производительность Queue с блокировкой и ConcurrentQueue.

person Alexey Kukanov    schedule 01.06.2011

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

Причина в том, что два (или более) потока, которые обращаются к ресурсу, могут попытаться получить к нему доступ в разное время — точно, когда каждый из них попытается получить к нему доступ, будет зависеть от многих факторов (насколько быстро ваш процессор то есть, сколько процессорных ядер доступно, какие другие программы выполняются в это время, работаете ли вы с выпуском или отладочной сборкой, работаете под отладчиком и т. д.). Вы можете запускать его много раз без появления сбоя, а затем он внезапно и «необъяснимо» выходит из строя - это может сделать эти ошибки чрезвычайно трудными для отслеживания, потому что они не часто появляются, когда вы пишете ошибочный код, но чаще, когда вы пишете другой несвязанный фрагмент кода.

Если вы собираетесь использовать многопоточность, очень важно, чтобы вы прочитали эту тему и поняли, что может пойти не так, когда и как правильно с этим справиться - неправильное использование блокировки может быть столь же опасным (если не более опасным). ), чем вообще не использовать блокировки (блокировка может привести к взаимоблокировкам, когда ваша программа просто «заблокируется»). К этому виду программирования нужно подходить осторожно!

person Jason Williams    schedule 01.06.2011

Да, этот код не работает. Очередь должна поддерживать многопоточность. Используйте ConcurrentQueue. См. http://msdn.microsoft.com/en-us/library/dd267265.aspx

person Richard Schneider    schedule 01.06.2011

Запустив ваш код, я получил InvalidOperationException - «Коллекция была изменена после создания экземпляра перечислителя». Это означает, что вы модифицируете данные, используя несколько потоков.

Вы можете использовать lock каждый раз, когда вы Enqueue или Dequeue, потому что вы изменяете очередь из нескольких потоков. Гораздо лучший вариант — использовать ConcurentQueues, так как он является потокобезопасным и блокирует -бесплатная параллельная коллекция. Это также обеспечивает лучшую производительность.

person oleksii    schedule 01.06.2011
comment
Это действительно верное утверждение, что параллельные классы данных не блокируются, или вы имели в виду, что код, который МЫ ПИШЕМ, будет свободен от блокировок? - person Chris Marisic; 02.06.2011
comment
ConcurrentQueue не блокируется. Другие классы в System.Collections.Concurrent используют блокировку. - person Jake T.; 02.06.2011
comment
@Chris ConcurrentQueue не блокируется. - person oleksii; 02.06.2011
comment
@oleksii очень хорошая ссылка, я раньше не видел эту статью в википедии - person Chris Marisic; 02.06.2011
comment
@ Крис, как ты имеешь в виду, звучит как сарказм, ты не согласен с тем, что ConcurentQueue не имеет блокировки? - person oleksii; 02.06.2011
comment
Я никогда не видел эту статью в Википедии, было очень интересно прочитать. - person Chris Marisic; 02.06.2011

Да, вы обязательно должны синхронизировать доступ к Queue, чтобы сделать его потокобезопасным. Но у вас другая проблема. Не существует механизма, который удерживает потребителей от бешеного вращения по петле. Синхронизация доступа к Queue или использование ConcurrentQueue не решит эту проблему.

Самый простой способ реализовать шаблон производитель-потребитель — использовать блокирующую очередь. К счастью, .NET 4.0 предоставляет BlockingCollection, который, несмотря на название, является реализацией блокирующей очереди.

public class Runner
{
    private BlockingCollection<int> queue = new BlockingCollection<int>();
    private Random random = new Random();

    public Runner()
    {
        for (int i = 0; i < 2; i++)
        {
            var thread = new Thread(Produce);
            thread.Start();
        }

        for (int i = 0; i < 2; i++)
        {
            var thread = new Thread(Consume);
            thread.Start();
        }
    }

    protected void Produce()
    {
        while (true)
        {
            int number = random.Next(0, 99);
            queue.Add(number);
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " Produce: " + number);
        }
    }

    protected void Consume()
    {
        while (true)
        {
            int number = queue.Take();
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " Consume: " + number);
        }
    }
}
person Brian Gideon    schedule 02.06.2011