programing

인쇄문이 없으면 루프가 다른 스레드에 의해 변경된 값을 볼 수 없습니다.

shortcode 2023. 1. 28. 09:50
반응형

인쇄문이 없으면 루프가 다른 스레드에 의해 변경된 값을 볼 수 없습니다.

코드에는 다른 스레드에서 어떤 상태가 변경되기를 기다리는 루프가 있습니다.다른 스레드는 동작하지만 루프는 변경된 값을 인식하지 않습니다.그것은 영원히 기다린다.하지만, 제가 이걸 넣었을 때System.out.println갑자기 작동한다구요! 왜요?


다음은 내 코드의 예입니다.

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (pizzaArrived == false) {
            //System.out.println("waiting");
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        pizzaArrived = true;
    }
}

while 루프가 실행 중일 때deliverPizza()다른 스레드에서 설정하기 위해pizzaArrived변수.하지만 루프는 내가 코멘트를 해제했을 때만 작동합니다.System.out.println("waiting");진술.무슨 일이야?

JVM은 다른 스레드가 다음 스레드를 변경하지 않는다고 가정할 수 있습니다.pizzaArrived변수입니다.다른 말로 하면, 이 기어는pizzaArrived == false루프 외부 테스트, 최적화:

while (pizzaArrived == false) {}

다음과 같이 입력합니다.

if (pizzaArrived == false) while (true) {}

무한 루프입니다.

한 스레드의 변경 내용을 다른 스레드에 표시하려면 항상 스레드 간에 동기화를 추가해야 합니다.이를 위한 가장 간단한 방법은 공유 변수를volatile:

volatile boolean pizzaArrived = false;

변수 만들기volatile는 서로 다른 스레드가 서로 변경된 영향을 볼 수 있음을 보증합니다.이로 인해 JVM이 다음 값을 캐싱할 수 없게 됩니다.pizzaArrived또는 루프 외부에 테스트를 호이스트합니다.대신 매번 실제 변수 값을 읽어야 합니다.

(좀 더 형식적으로 말하면,volatile는 변수에 대한 액세스 간에 operations-before 관계를 만듭니다.이것은 피자를 배달하기 전에 스레드가 했던 다른 모든 작업이 피자를 받는 스레드에도 보여진다는 것을 의미합니다, 비록 다른 변화들이 피자에 영향을 주지 않을지라도.volatile변수)

동기화된 메서드는 주로 상호 제외(동시에 발생하는 두 가지 일을 방지)를 구현하기 위해 사용되지만 다음과 같은 모든 부작용이 있습니다.volatile변수를 읽고 쓸 때 변수를 사용하는 것도 다른 스레드에 변경 내용을 표시할 수 있는 다른 방법입니다.

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (getPizzaArrived() == false) {}
        System.out.println("That was delicious!");
    }

    synchronized boolean getPizzaArrived() {
        return pizzaArrived;
    }

    synchronized void deliverPizza() {
        pizzaArrived = true;
    }
}

인쇄문의 효과

System.out는 입니다.PrintStream물건.의 방법PrintStream는 다음과 같이 동기화됩니다.

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

동기화에 의해,pizzaArrived루프가 진행되는 동안 캐시됩니다.엄밀히 말하면, 변수에 대한 변경을 확실히 표시하기 위해서, 양쪽스레드같은 오브젝트로 동기 할 필요가 있습니다.(를 들면, 콜)println설정 후pizzaArrived그리고 읽기 전에 다시 전화한다.pizzaArrived맞다고 생각합니다.)특정 개체에서 하나의 스레드만 동기화되는 경우 JVM은 이를 무시할 수 있습니다.실제로 JVM은 다른 스레드가 호출하지 않는다는 것을 증명할 만큼 충분히 스마트하지 않습니다.println설정 후pizzaArrived아마 그럴 거라고 추측하고 있어요따라서 루프 중에 변수를 캐시할 수 없습니다.System.out.println그렇기 때문에 올바른 수정은 아니지만 인쇄문이 있는 경우 이와 같은 루프가 작동합니다.

사용.System.out루프가 작동하지 않는 이유를 디버깅하려고 할 때 가장 자주 발견되는 것이 이 효과를 일으키는 유일한 방법은 아닙니다.


더 큰 문제는

while (pizzaArrived == false) {}busy-wait 루프입니다.큰일 났다!대기하는 동안 CPU를 독점하여 다른 응용 프로그램의 속도가 느려지고 시스템의 전력 사용량, 온도 및 팬 속도가 증가합니다.루프 스레드가 대기하는 동안 sleeve 상태가 되어 CPU를 독점하지 않는 것이 이상적입니다.

이를 위한 몇 가지 방법은 다음과 같습니다.

wait/notify 사용

낮은 수준의 솔루션은 다음과 같은 wait/notify 방법을 사용하는 것입니다.

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        synchronized (this) {
            while (!pizzaArrived) {
                try {
                    this.wait();
                } catch (InterruptedException e) {}
            }
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        synchronized (this) {
            pizzaArrived = true;
            this.notifyAll();
        }
    }
}

이 버전의 코드에서는 루프 스레드가 를 호출하여 스레드를 sleep 상태로 만듭니다.sleep 중에는 CPU 사이클을 사용하지 않습니다.두 번째 스레드는 변수를 설정한 후 해당 개체에서 대기하고 있던 모든 스레드를 웨이크업하도록 호출합니다.이것은 피자 배달원이 초인종을 누르도록 하는 것과 같아서, 당신은 문 앞에 어색하게 서 있는 대신 앉아서 쉴 수 있다.

오브젝트에서 wait/notify를 호출할 때는 해당 오브젝트의 동기화 잠금을 유지해야 합니다.이것이 위의 코드의 동작입니다.두 스레드가 같은 오브젝트를 사용하는 한 원하는 오브젝트를 사용할 수 있습니다.this(의 인스턴스MyHouse일반적으로 2개의 스레드는 같은 오브젝트의 동기화된 블록(동기 목적의 일부)을 동시에 입력할 수 없습니다.단, 스레드가 내부에 있을 때 동기 잠금을 일시적으로 해제하기 때문에 여기서 동작합니다.wait()방법.

블로킹 큐

A는 생산자/소비자 큐를 구현하기 위해 사용됩니다."소비자"는 큐의 맨 앞부분에서 아이템을 가져가고, "프로듀서"는 뒤에 아이템을 밀어 넣습니다.예:

class MyHouse {
    final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();

    void eatFood() throws InterruptedException {
        // take next item from the queue (sleeps while waiting)
        Object food = queue.take();
        // and do something with it
        System.out.println("Eating: " + food);
    }

    void deliverPizza() throws InterruptedException {
        // in producer threads, we push items on to the queue.
        // if there is space in the queue we can return immediately;
        // the consumer thread(s) will get to it later
        queue.put("A delicious pizza");
    }
}

주의:put그리고.take방법BlockingQueue던질 수 있다InterruptedExceptions. 이것은 처리해야 하는 체크 예외입니다.위의 코드에서는, 간단하게 하기 위해서, 예외가 리트루닝 됩니다.메서드 내의 예외를 검출하여 put 또는 take 콜을 재시도하여 성공 여부를 확인할 수 있습니다.그 추악한 점 하나 빼고BlockingQueue매우 사용하기 쉽습니다.

여기에서는 다른 동기화가 필요하지 않습니다.BlockingQueue는 항목을 큐에 넣기 전에 스레드가 수행한 모든 작업을 스레드에서 볼 수 있도록 합니다.

실행자

Executor는 기성품이나 다름없다.BlockingQueue작업을 실행합니다.예:

// A "SingleThreadExecutor" has one work thread and an unlimited queue
ExecutorService executor = Executors.newSingleThreadExecutor();

Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };

// we submit tasks which will be executed on the work thread
executor.execute(eatPizza);
executor.execute(cleanUp);
// we continue immediately without needing to wait for the tasks to finish

상세한 것에 대하여는, 및 의 메뉴얼을 참조해 주세요.

이벤트 처리

사용자가 UI에서 무언가를 클릭할 때까지 기다리는 동안 루프가 발생하는 것은 잘못된 것입니다.대신 UI 툴킷의 이벤트 처리 기능을 사용하십시오.Swing의 예:

JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
    // This event listener is run when the button is clicked.
    // We don't need to loop while waiting.
    label.setText("Button was clicked");
});

이벤트 핸들러는 이벤트디스패치 스레드에서 실행되므로 이벤트핸들러에서 장시간 작업을 하면 작업이 완료될 때까지 UI와의 다른 상호작용이 차단됩니다.저속 동작은 새로운 스레드에서 시작하거나 위의 방법 중 하나를 사용하여 대기 스레드로 디스패치할 수 있습니다(대기/통지,BlockingQueue, 또는Executor를 사용할 수도 있습니다.이러한 설계로 백그라운드워커 스레드가 자동으로 제공됩니다.

JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");

// Add a click listener for the button
button.addActionListener((ActionEvent e) -> {

    // Defines MyWorker as a SwingWorker whose result type is String:
    class MyWorker extends SwingWorker<String,Void> {
        @Override
        public String doInBackground() throws Exception {
            // This method is called on a background thread.
            // You can do long work here without blocking the UI.
            // This is just an example:
            Thread.sleep(5000);
            return "Answer is 42";
        }

        @Override
        protected void done() {
            // This method is called on the Swing thread once the work is done
            String result;
            try {
                result = get();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            label.setText(result); // will display "Answer is 42"
        }
    }

    // Start the worker
    new MyWorker().execute();
});

타이머

정기적인 액션을 수행하려면 를 사용합니다.이 방법은 자신의 타이밍 루프를 쓰는 것보다 사용하기 쉽고 시작과 정지가 용이합니다.이 데모에서는 현재 시간을 초당 1회 인쇄합니다.

Timer timer = new Timer();
TimerTask task = new TimerTask() {
    @Override
    public void run() {
        System.out.println(System.currentTimeMillis());
    }
};
timer.scheduleAtFixedRate(task, 0, 1000);

각각java.util.Timer스케줄된 실행을 위해 사용되는 자체 백그라운드 스레드가 있습니다.TimerTasks. 당연히 스레드는 작업 간에 sleep 상태가 되기 때문에 CPU를 독점하지 않습니다.

Swing 코드에는 유사한 가 있지만 Swing 스레드에서 리스너를 실행하므로 수동으로 스레드를 전환하지 않고도 Swing 컴포넌트와 안전하게 대화할 수 있습니다.

JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(1000, (ActionEvent e) -> {
    frame.setTitle(String.valueOf(System.currentTimeMillis()));
});
timer.setRepeats(true);
timer.start();
frame.setVisible(true);

Other ways

If you are writing multithreaded code, it is worth exploring the classes in these packages to see what is available:

And also see the Concurrency section of the Java tutorials. Multithreading is complicated, but there is lots of help available!

ReferenceURL : https://stackoverflow.com/questions/25425130/loop-doesnt-see-value-changed-by-other-thread-without-a-print-statement

반응형