Многопоточная программа не работает должным образом

Я пытался узнать о семафорах, читая «Маленькую книгу семафоров» Аллена Б. Дауни.

В этом есть загадка:

Обобщите решение встречи. Каждый поток должен запускать следующий код:

1 rendezvous
2 critical point

Требование синхронизации состоит в том, чтобы ни один поток не выполнял критическую точку до тех пор, пока все потоки не выполнили рандеву. Вы можете предположить, что существует n потоков и что это значение хранится в переменной n, доступной из всех потоков. Когда прибывают первые n - 1 потоков, они должны блокироваться до тех пор, пока не прибудет n-й поток, после чего все потоки могут продолжить работу.

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

Я не могу понять, в чем тут дело

Вот мое решение кода -

import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;

class Main {
    
    private static final int n = 10;
    private static int counter = 0;
    private static final Semaphore mutex = new Semaphore(1);
    private static final Semaphore barrier = new Semaphore(0);

    private static class Runner implements Runnable {
        
        @Override
        public void run() {
            try {
                System.out.println("Rendezvous");
                mutex.acquire();
                counter = counter + 1;
                mutex.release();
                while (counter < n);
                barrier.release();
                barrier.acquire();
                System.out.println("Critical Section");
                barrier.release();
            } catch (InterruptedException ex) {
                System.out.println("Got exception while executing code for runner : " + ex.getMessage());
            }
        }
    }
    
    public static void main(String[] args) {
        List<Thread> threads = new ArrayList<>();
        for (int i = 0;i < n;i++) {
            Thread t = new Thread(new Runner());
            t.start();
            threads.add(t);
        }
        
        for (Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException ex) {
                System.out.println("Got exception while executing code for runner : " + ex.getMessage());
            }
        }
        
        
        System.out.println("Completed");
    }
}

Это твой настоящий код? Чего вы ожидаете while (counter < n); достичь?

PM 77-1 11.07.2024 22:30

См. мой ответ ниже о том, почему код не работает. Я также хотел отметить, что использование барьера в его нынешнем виде ничего не дает. Он выпускается до того, как будет получен, поэтому aquire() гарантированно всегда получит разрешение.

OfNothing 12.07.2024 04:22

Я понимаю, что вы делаете это в качестве упражнения для понимания семафоров, но в реальном коде лучше использовать CountDownLatch.

OfNothing 12.07.2024 04:44
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
3
76
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема с кодом:

 while (counter < n);

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

Но вывод должен быть таким: если общее целое число не защищено мьютексом, как в данном случае, AtomicInteger можно использовать для принудительной синхронизации значения со всеми потоками и обойти вышеупомянутую оптимизацию следующим образом:

counter.incrementAndGet();
while( counter.get() < n );

Ему даже не нужен мьютекс.

Однако опрос значения счетчика подобным образом не является хорошей идеей, потому что большую часть времени ЦП будет просто вращаться, добавляя тепла во Вселенную. Есть лучший способ сделать это.

Вот очень щедрый совет о том, как решить головоломку: представьте, что есть N потоков, ожидающих получения() семафора, у которого нет разрешений. Если вы Release() N разрешите семафор, все потоки продолжат работу.

Хорошо, понял! Таким образом, помимо использования AtomicInteger, если бы я объявил счетчик как изменчивую переменную, этот код должен был бы работать и в этом случае. Также я нашел еще одну ссылку, которая помогла мне понять, как регистры кэшируют переменные в Java — stackoverflow.com/questions/5022100/…

ishaank10 13.07.2024 10:01

Однако здесь я задаю один вопрос: если предположить, что проблема возникает из-за кэширования счетчика в регистре, то эта проблема также может возникнуть при увеличении/уменьшении значения внутри мьютекса. Так как же это работает правильно?

ishaank10 13.07.2024 16:18

@ishaank10 Что касается второго вопроса: я не уверен, какой механизм Java использует с мьютексами для предотвращения кэширования локального потока. Если вы зайдете в кроличью нору и вытащите исходный код семафоров, вы обнаружите, что он будет вызывать некоторые внутренние функции jvm, с которыми, я полагаю, компилятор и оптимизатор знают, что лучше не связываться. Если вам интересна эта тема, предлагаю поискать «заборы памяти» или «барьеры памяти».

OfNothing 14.07.2024 16:33

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