2. 스레드와 잠금장치

이영규·2023년 4월 4일
0

7가지동시성모델

목록 보기
1/1
  • 스레드와 잠금장치를 이용하는 프로그래밍은 원시적이고 어려우며, 안정성도 떨어지고 위험하다.
  • 그럼에도 불구하고 스레드와 잠금장치는 많은 동시성 소프트웨어 작성에서 기본 선택이 되고 있으며, 때문에 그 동작 원리를 이해할 필요가 있다.

2.1 동작하는 가장 단순한 코드

  • 스레드와 잠금장치는 실제 하드웨어가 동작하는 방식을 그대로 옮긴 것과 거의 동일하다.

2.2 상호배제와 메모리 모델

  • 알아야 하는 개념 : 상호배제(mutual exclusion), 경쟁 조건(race condition), 데드락(deadlock), 메모리 모델

2.2.1 스레드 만들기

thread.start(); // 스레드 인스턴스를 생성하고 시작, 스레드의 run()과 메인스레드가 동시에 실행
thread.join();  // 해당 스레드가 동작을 멈출 때까지(run()이 리턴할 때까지) 대기

2.2.2 첫번째 잠금장치

  • 여러 개의 스레드가 공유된 메모리에 접근할 때에는 서로 동작이 엉킬 수 있음
  • 여러 스레드에서 잠금장치 없이 공유 메모리에 접근하면, 실행할 때마다 다른 결과를 얻게 됨
  • 이는 스레드들이 메모리의 값을 읽을 때 발생하는 경쟁 조건 때문임
  • count++ 의 경우 컴파일러가 만드는 바이크 코드는 다음과 같음
getfield #2 // count 값을 읽음
iconst_1
iadd        // 1을 더함
putfield #2 // 결과가 count에 저장
  • 읽기 -> 수정하기 -> 쓰기
  • 여러 스레드가 같은 값을 읽어와 같은 값을 쓰기하면서 count 가 적절하게 증가되지 않음
  • 우리는 한 번에 하나의 스레드만 보유할 수 있는 잠금장치를 사용해 이러한 상황을 피할 수 있음
    • 즉 공유 메모리의 값을 동기화 하는 것
  • 자바에는 잠금장치가 내재되어 있음(mutex, monitor 혹은 critical section 이라고 불리는)
public synchronized void increment() { ++count; }
  • synchronized 키워드가 있는 메서드를 호출하면 잠금장치를 요구
  • 리턴할 때는 잠금장치도 해제됨
  • 한 번에 오직 하나의 스레드만 이 메서드를 호출할 수 있으며, 동시에 접근하는 다른 스레드들은 잠금장치가 해제될 때까지 블로킹됨

2.2.3 메모리의 미스터리

  • 컴파일러는 코드가 실행되는 순서를 바꿈으로써 정적 최적화를 수행할 수 있다.
  • JVM은 코드가 실행되는 순서를 바꿈으로써 동작 최적화를 수행할 수 있다.
  • 코드를 실행하는 하드웨어도 코드의 순서를 바꾸는 것이 가능하다.
  • 이는 공유 메모리를 사용하는 병렬 컴퓨터에 필요한 최적화 부분으로 이 부분을 받아들이고 다룰 수 있어야 함
  • 어떤 부분까지 신뢰할 수 있는지 정해둔 규칙이 자바 메모리 모델

2.2.4 메모리 가시성

  • 흔히 간과하는 사실 : 스레드가 모두 동기화 되어야 함
    • 값을 변경하는 스레드 뿐만 아니라, 값을 조회하는 스레드도 동기화 되어야 한다.
  • 잘못된 값을 조회할 수 있기 때문

2.2.5 여러 개의 잠금장치

  • 멀티 스레드 환경에서 안정성을 보장하는 유일한 방법이 모든 메서드를 동기화 하는 것처럼 보일 수 있음
  • 이 경우 효율성이 극도로 떨어짐
  • 대부분의 스레드가 블로킹 된 상태에서 대기하게 될 것
  • 추가로, 둘 이상의 스레드에서 동기화하는 경우 데드락이 발생할 가능성이 매우 높음

식사하는 철학자

  • 철학자는 배가 고프면 양쪽의 젓가락을 집어올리고 한동안 식사한다.
  • 식사가 끝나면 젓가락을 내려놓는다.
  • 모든 철학자가 동시에 식사하기로 마음을 먹는 경우,
  • 모두 자기 왼쪽의 젓가락을 들어올리고, 오른 쪽의 철학자가 젓가락을 내려놓기 전까지 동작을 멈추게 됨
  • 그 순간은 영원히 오지 않음 = 데드락
  • 데드락의 위험성은 어떤 스레드가 둘 이상의 잠금장치를 손에 넣으려고 할 때 반드시 존재하게 됨!
  • 이를 해결하기 위해서는 잠금장치를 요청할 때 항상 정해진 공통의 순서를 따르면 됨
  • 예를 들어, 왼쪽 -> 오른쪽의 순서로 젓가락을 집는 게 아니라, (젓가락 마다 숫자로 이루어진 고유한 ID가 주어지고) ID가 낮은 것부터 집는다고 가정
  • 이렇게 하면 동작을 멈추는 일 없이 영원히 동작이 수행됨!
    • Min(left, right) != Max(all)
    • 처음 집는 젓가락은 ID 의 최댓값이 될 수 없음 -> 데드락이 발생하지 않음(누군가는 두번쨰로 최댓값을 집고 락을 해제할 것)

2.2.6 외부 메서드의 위험

  • 하나의 장소에서 잠금장치를 얻는 경우, 공통 순서를 부과하는 것이 쉬움
  • 하지만 커다란 프로그램에서는 모든 코드가 공통 규칙을 따르기 어려움
  • 따라서 데드락을 방지하기 위해서는 다음의 원칙을 지킬 필요가 있음

어떤 잠금 장치를 보유하고 있는 상태에서는 외부 메서드를 호출하지 않는다.

private ArrayList<Listener> listenrs;

private synchronized void updateProgress(int n) {	// 메서드 전체 락
  for(Listener listener : listeners) {
    listener.onProgress(n);
  }
}
  • 원칙에 따라 위의 코드는 아래와 같이 바꿀 수 있음
private ArrayList<Listener> listenrs;

private synchronized void updateProgress(int n) {
  ArrayList<Listener> listenrsCopy;
  
  synchronized(this) {	// 복사 하는 부분만 락
    listenrsCopy = listeners.clone();	// 얕은 복사
  }
  
  for(Listener listener : listenrsCopy) {
    listener.onProgress(n);
  }
}
  • 리스너 목록에 락을 잡으면, onProgress() 에서 데드락이 발생할 수 있음
  • 반면, clone() 중에만 락을 잡으면 데드락도 피하고 성능 상 이점도 가져갈 수 있음
    • 락을 잡는 시간이 줄어들어 성능 상 이점이 있음

정리

  • 공유 되는 변수에 대한 접근은 반드시 동기화한다.
  • 이 때 쓰는 스레드 뿐만 아니라 읽는 스레드도 동기화 되어야 한다.
  • 여러 개의 잠금장치를 동시에 사용하지 않는다.
    • 동시에 사용하는 경우 데드락이 발생할 수 있다.
    • 잠금 장치를 가진 상태에서는 외부 메서드를 호출하지 않는다.
    • 여러 개의 잠금장치를 사용하는 경우 미리 정해진 공통의 순서에 따라 요청한다.
  • 잠금장치는 최대한 짧게 보유한다.
profile
더 빠르게 더 많이 성장하고 싶은 개발자입니다

0개의 댓글