MultiThread 환경에서 CPU Cache 변수 불일치 문제 해결하기 (with volatile)

Damongsanga·2024년 3월 26일
0

BBoard 프로젝트에서 비동기 멀티스레딩을 활용하여 네트워크 I/O를 줄여본 경험이 있었지만, 스레드들이 어떻게 자원을 공유하고, 어떠한 문제를 유발할 수 있는지에 대한 이해가 부족했던 것 같아 강의를 들으며 글로 정리해보고자 한다. 오늘 내용은 CPU Cache에 의한 데이터 정합성 문제와 이에 대한 해결 방법을 알아보고자 한다.

멀티 쓰레드 이해하고 통찰력 키우기 강의 내용을 학습한 내용을 정리한 글입니다.


  • CPU Cache

    • Cache가 멀티스레드일 때 예상치 못한 문제를 발생시킬 수 있다!
    1. Stale Data (오래된 데이터)
      • 각자의 CPU는 각자의 캐시를 가지고 CPU 1개는 1개의 스레드를 실행한다.
      • 한 변수를 2개의 스레드가 수정 및 조회할 때 한쪽 스레드가 수정한 내용을 캐시를 읽는 바람에 모를 수 있다
    2. ReOrdering
      • 코드 최적화로 인해 코드의 순서가 바뀔 수 있고 이 과정에서 다른 스레드의 작동에 문제가 생길 수 있다.
      • 항상 Release Mode로 해야한다!
  • 해결 방법

    1. Visibility
      • 가시성을 부여하면 해당 메모리는 반드시 메인 메모리 값을 읽어오게 되어 Stale Data 문제 해결할 수 있다.
    2. Atomicity
      • 반드시 하나의 일관된 동작이 한 Thread에서만 시작~종료되면 이를 Atomic Operation이라고 한다
      • 예시로 증가연산자는 atomic operation이 아니다!
      • 자바에서는 Atomic Integer 등을 사용할 수 있다.
    3. Reordering 방지
      • Volatile.Write
      • Volatile.Read
      • C++에서 사용되는 해당 함수의 전후관계는 반드시 유지되어 Reordering을 방지할 수 있다.
  • 관련 키워드

    • 자바에서는 volatile 키워드 사용하여 해결
      • Visibility, Atomicity, Reordering 모두 제공한다
      • non-volatile인 경우, MultiThread 어플리케이션에서는 Task를 수행하는 동안 성능 향상을 위해 메인 메모리에서 읽은 변수 값을 CPU cache에 저장하고 그 곳에서 읽어오게 된다.
      • Multi Thread환경에서 Thread가 변수 값을 읽어올 때 각각의 CPU cache에 저장된 값이 다름으로, 변수 값 불일치 문제가 발생한다.
    • synchronized keyword
    • lock
    • Interocked.Increment ⇒ 자바는 Atomic Integer 사용
    • Memory Barrier 등!
  • +a) Context Switching

    • 컨텍스트 스위칭이 발생하면 "CPU Cache가 초기화된다!"
    • Sleep, Lock, IO 시 발생한다
    • read, write, 연산 외의 시스템 API 호출시 거의 발생한다

volatile 키워드를 사용한 멀티스레딩 코드 예시

강의에서 제공한 원본 코드 github가 C++로 되어있어 이를 바탕으로 Java로 새로 코드를 작성해보았다.

상황

해당 코드 내에서는 main 스레드에서 thread1 이라는 스레드를 생성하였고, thread1을 실행시키기 위해 Thread.sleep()을 주어 context switching이 발생하도록 하였다.

이후 메인 스레드에서 shouldStop 변수를 true로 변경하였으나, thread1에서 먼저 읽은 shouldStop 값은 false이고, 이를 CPU Cache에서 읽어오기 때문에 변경된 사실을 알지 못한다. 이로 인해 while문을 빠져나오지 못한다.

public class MultiThreadTest {

    private static boolean shouldStop = false; // 두 Thread가 공유하는 변수

    static class MyThread extends Thread{
        public void run(){
            boolean toggle = false;
            System.out.println("working...");
            while(shouldStop == false){
                toggle = !toggle;
            }
            System.out.println("done!");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("Process Start!");

        Thread thread1 = new MyThread();
        thread1.start();

        Thread.sleep(1000); // thread1로 실행하기 위해서 context switching 발생시킴

        shouldStop = true; // 변수를 변경하였으나 thread1에서는 CPU cache에서 데이터를 읽기 때문에 이를 감지하지 못함
        thread1.join();

        System.out.println("All Done!");

    }

}
  • while loop를 벗어날 수 없다.
Process Start!
working...

volatile 키워드 사용

  • volatile 키워드를 사용하여 메인 메모리에서 값을 읽어오도록 한다.
  • CPU Cache에서 읽어오지 않게 되어 Stale Data 대신 최신 데이터를 읽어오게 되어 while loop를 빠져나올 수 있다.
private static volatile boolean shouldStop = false;
  • 성공적으로 탈출하는 것을 알 수 있다!
Process Start!
working...
done!
All Done!
profile
향유하는 개발자

0개의 댓글