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

#c# #multithreading #concurrency #producer-consumer

#c# #многопоточность #параллелизм #производитель-потребитель

Вопрос:

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

Мой код выглядит следующим образом

     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");
                }
        }
    }
}
  

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

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

Заранее благодарю вас! =)

Ответ №1:

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

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

Ответ №2:

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

Ответ №3:

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

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

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

Ответ №4:

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

Ответ №5:

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

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

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

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

2. ConcurrentQueue без блокировки. Другие классы в System.Collections.Concurrent используют блокировку.

3. @Chris ConcurrentQueue не заблокирован .

4. @oleksii очень хорошая ссылка, я раньше не видел эту статью в википедии

5. @Chris что вы имеете в виду, звучит как сарказм, вы не согласны с тем, что ConcurentQueue не имеет блокировок?

Ответ №6:

Да, вы определенно должны синхронизировать доступ к 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);
        }
    }
}