프롤로그
가시성과 원자성 이 두가지를 이해 하면 Multi-thread프로로그램을 작성할 때 무엇을 주의해하는지 명확해진다. 또 다른 표현으로 설명 하자면 단일 Thread 프로그램에서는 가시성과 원자성을 괘념치 않아도 프로그램 작성하는데 문제가 없다. 그렇다고 해서 가시성과 원자성이 Multi-thread를 할 때만 뚜둥 나타나는 개념은 아니라는점 명확히 밝혀둔다. 하드웨어를 설계한 사람들에 의해 만들어진 원래 컴퓨터 내의 구조에 관한 이야기다. 이번 편에서는 지난편에서 설명한 이 두 가지 개념중에 가시성을 좀더 깊게 파고들어보려 한다.
비 가시성(가시성 이슈)
다른 자료나 책을 통해 가시성에 대한 개념을 이해하고 있는 독자라면 필자가 외 비 가시성이라는 반대가 되는 단어를 만들어 썼는지 감이 왔을것이다. 지난 편에도 아래 그림이 등장했는데 다시한번 고찰 해보자.
각기 다른 Thread 2개는 CPU 1과 CPU2를 할당 받아 공유자원에 해당하는 변수를 연산한다.
이 때 메인 메모리에서 값을 읽어 연산에 사용하는 것이 아니라 가 CPU에 존재하는 Cache에 옮겨놓고 연산을 한다.
그 사이에 다른 쓰레드에서 같은 변수를 대상으로 연산을 한다. 이 때 Thread는 타 Thread가 사용하고 있는 CPU의 Cache상의 값을 알지 못한다. 언제 Cache의 값이 메인 메모리로 쓰여질지도 모른다.
그래서 아래코드를 돌려보면 각각의 Thread가 100회씩 변수를 증가연산을 실시 했는데 200에 휠씬 못 미치는 103 ~ 105쯤이 나타난다. (이 실험 결과는 하드웨어의 성능에 따라 상이 할 수 있으니 참고 바란다.) 만약 동시에 시작하는 Thread가 아닌 순차 처리라면 200이 나와야 하는 현상이다. Cache에 담아서 연산을 하더라도 바로 바로 메모리에 적용을 했으면 200은 아니더라도 180~190정도는 날올것 같은데 너무나도 100가까운 결과에 놀라는 독자도 있을 것이다. 다음 단계에서 이 문제를 조금(?) 해결 해보자.
가시성이란 멀티 코어의 캐시(Cache) 간 불일치의 문제를 말하는데, 하나의 스레드에서 특정한 변수의 값을 수정하였는데, 다른 스레드에서 그 수정된 값을 제대로 읽어 들인다는 보장이 없다.
(참고로 C++의 volatile은 JAVA와는 달리 가시성 문제를 해결해주지 않는다. 단순히 컴파일러 최적화를 막기만 함)
* Java 에서 가시성 관련된 이야기 시작 암튼 JAVA에선 위에서 설명한 비 가시성 이슈를 해결하기 위해 JAVA 1.4 부터 volatile을 지원하기 시작 했다. 변수를 선언 할 때 해당 단어를 앞에 써주기만 하면 되는데 이렇게만 해도 위 테스트 코드가 거의 200을 반환한다. (사실 이 결과는 미신같은 이야기다. 정확히 이야기 하면 200이거나 200보다 작은 값을 반환한다.) 원리는 이렇다. Java 에선 volatile로 선언된 변수를 CPU에서 연산을 하면 바로 메모리에 쓴다. (Cache에서 메모리로 값이 이동하는 것을 다른 책이나 문서에서는 flush라고 표현한다.) 그러니 운이 좋게 Thread 두 개가 주거니 받거니 하면서 증가를 시키면 200에 가까운 결과를 얻어내는 것이다. 하지만 개발자라면 이 미신같은 결과에 흡족해 하면 안된다. 필자의 PC를 기준으로 각 Thread당 100회가 아닌 1000회정도 연산을 시키면 2000이 아닌 1998같은 결과를 얻어낸다. 이 이야기인 즉 가시성이 확보된다 하더라도 원자성 문제(동시에 같은 값을 읽어다 증가시키고 flush하는...)로 인해 이와 같은 문제가 생기는 것이다. 이 문제는 원자성 다루면서 해결 해보자. * Java 에서 가시성 관련된 이야기 끝 |
아래 코드는 자바이지만 제대로 되지 않는 다는 정도만 보면 된다
private int count = 0;
@Test
public void Test_AtomicIssue() {
ExecutorService es = Executors.newFixedThreadPool(2);
es.execute(new ForThreadTest());
es.execute(new ForThreadTest());
es.shutdown();
try {
es.awaitTermination(10, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("TEST result >>> " + count);
}
class ForThreadTest implements Runnable {
@Override
public void run() {
for(int i = 0 ; i < 100; i++) {
AtomicStampedRefTest.this.count++;
}
}
}
'운영체제 & 병렬처리 > Multithread' 카테고리의 다른 글
코드 재배치 및 메모리 가시성문제 예제 (0) | 2022.09.12 |
---|---|
CPU 파이프라인과 코드 재배치 (0) | 2022.09.12 |
Cache (2) - 코어당 캐시메모리 (0) | 2022.09.12 |
Cache (1) - 존재하는 이유와 성능 차이 (0) | 2022.09.11 |
future , async, promise, packaged_task (2) (0) | 2022.09.11 |