Циркулярный буфер с Threads Consumer и Producer: он зависает при выполнении некоторых программ

Я разрабатываю кольцевой буфер с двумя потоками: потребитель и производитель. Я использую активное ожидание с Thread.yield, Я знаю, что это можно сделать с семафорами, но я хотел буфер без семафоров.

Оба имеют общую переменную: bufferCircular,

Хотя буфер не полон полезной информации, producer написать данные в позиции pмассива, и в то время как есть некоторая полезная информация consumer читать данные в позиции c массива. Переменная nElem от BufferCircular количество значений данных, которые еще не были прочитаны

Программа работает неплохо, работает 9/10 раз. Затем, иногда, он застревает в бесконечном цикле, прежде чем показывать последний элемент на экране (номер 500 цикла для), или просто не показывает какой-либо элемент.

Я думаю, что это, вероятно, liveLock, но я не могу найти ошибку.

Общая переменная:

public class BufferCircular {
    volatile int[] array;
    volatile int p;
    volatile int c;
    volatile int nElem;

    public BufferCircular(int[] array) {
       this.array = array;
       this.p = 0;
       this.c = 0;
       this.nElem = 0;
    }

    public void writeData (int data) {
       this.array[p] = data;
       this.p = (p + 1) % array.length;
       this.nElem++;
    }

    public int readData() {
       int data = array[c];
       this.c = (c + 1) % array.length;
       this.nElem--;
       return data;
    }

}

Автор темы:

public class Producer extends Thread {
    BufferCircular buffer;
    int bufferTam;
    int contData;

    public Productor(BufferCircular buff) {
       this.buffer = buff;
       this.bufferTam = buffer.array.length;
       this.contData = 0;

    }

    public void produceData() {
       this.contData++;
       this.buffer.writeData(contData);
    }

    public void run() {
       for (int i = 0; i < 500; i++) {
          while (this.buffer.nElem == this.bufferTam) {
          Thread.yield();
        }
          this.produceData();
       }
    }
}

Потребительская нить:

    public class Consumer extends Thread {
        BufferCircular buffer;
        int cont;

        public Consumer(BufferCircular buff) {
           this.buffer = buff;
           this.cont = 0;
        }

        public void consumeData() {
           int data = buffer.readData();
           cont++;
           System.out.println("data  " + cont + ": " + data);
        }

        public void run() {
           for (int i = 0; i < 500; i++) {
              while (this.buffer.nElem == 0) {
                 Thread.yield();
              }
               this.consumeData();
           }
        }
   }

Основное:

public class Main {

    public static void main(String[] args) {
       Random ran = new Random();
       int tamArray = ran.nextInt(21) + 1;
       int[] array = new int[tamArray];

       BufferCircular buffer = new BufferCircular(array);

       Producer producer = new Producer (buffer);
       Consumer consumer = new Consumer (buffer);

       producer.start();
       consumer.start();

       try {
           producer.join();
           consumer.join();
       } catch (InterruptedException e) {
           System.err.println("Error with Threads");
           e.printStackTrace();
    }

    }

}

Любая помощь будет приветствоваться.

1 ответ

Решение

Ваша проблема здесь в том, что ваш BufferCircular методы чувствительны к условиям гонки. Возьмите например writeData(), Он выполняется в 3 этапа, некоторые из которых также не являются атомарными:

this.array[p] = data;             // 1
this.p = (p + 1) % array.length;  // 2  not atomic
this.nElem++;                     // 3  not atomic

Предположим, что 2 темы введены writeData() в то же время. На шаге 1 они оба имеют одинаковые p значение, и оба переписать array[p] значение. Сейчас, array[p] перезаписывается дважды, и данные, которые должен был записать первый поток, теряются, потому что второй поток записывал в тот же индекс после. Затем они выполняют шаг 2- и результат непредсказуем, поскольку р можно увеличить на 1 или 2 (p = (p + 1) % array.length состоит из 3 операций, где потоки могут взаимодействовать). Затем шаг 3. ++ Оператор тоже не атомарный: он использует 2 операции за кулисами. Так nElem также увеличивается на 1 или 2.

Так что у нас совершенно непредсказуемый результат. Что приводит к плохому исполнению вашей программы.

Самое простое решение - сделать readData() а также writeData() методы сериализованы. Для этого объявить их synchronized:

public synchronized void writeData (int data) { //...
public synchronized void readData () { //...

Если у вас только один производитель и один потребительский поток, могут возникнуть условия гонки на операциях, связанных с nElem, Решение заключается в использовании AtomicInteger вместо int:

final AtomicInteger nElem = new AtomicInteger();

и использовать его incrementAndGet() а также decrementAndGet() методы.

Другие вопросы по тегам