13 동시성

Seunghee Ryu·2023년 12월 17일
0

클린 코드

목록 보기
13/18

  • 동시성이 필요한 이유와 그 어려움, 그리고 동시성을 유지하면서 깨끗한 코드를 작성하고 테스트하는 방법과 문제점에 대해 알려준다

동시성이 필요한 이유?

  • 동시성 = 결합을 없애는 전략
    - 무엇과 언제를 분리하는 전략
  • 스레드가 하나인 프로그램은 무엇과 언제가 서로 밀접하다
    - 단일 스레드 프로그램은 정지점을 정한 후 시스템 상태를 파악
  • 무엇과 언제를 분리하면 애플리케이션 구조와 효율이 극적으로 나아진다
    - 웹 애플리케이션이 표준으로 사용하는 서블릿 모델
    - 웹 혹은 EJB 컨테이너 아래에서 실행
    - 이 컨테이너는 동시성을 부분적으로 관리
    - 웹 요청이 들어오면 웹 서버는 비동기식으로 서블릿을 실행
    - 원칙적으로 각 서블릿 스레드는 다른 서블릿 스레드와 무관
  • 웹 컨테이너가 제공하는 결합분리 전략은 완벽하지 않음

미신과 오해

  • 동시성은 항상 성능을 높여준다
  • 동시성을 구현해도 설계는 변하지 않는다
  • 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다

타당한 생각

  • 동시성은 부하를 유발한다
  • 동시성은 복잡하다
  • 일반적으로 동시성 버그는 재현하기 어렵다
  • 동시성을 구현하려면 근본적인 설계 전략을 재고해야 한다

난관

  • 동시성을 구현하기 어려운 이유는?
public class ClassWithThreadingProblem {
    private int lastIdUsed;
    
    public ClassWithThreadingProblem(int lastIdUsed) {
        this.lastIdUsed = lastIdUsed;
    }
    
    public int getNextId() {
        return ++lastIdUsed;
    }
}

public static void main(String args[]) {
    final ClassWithThreadingProblem classWithThreadingProblem = new ClassWithThreadingProblem(42);
    
    Runnable runnable = new Runnable() {
        public void run() {
            classWithThreadingProblem.getNextId();
        }
    };
    
    Thread t1 = new Thread(runnable);
    Thread t2 = new Thread(runnable);
    t1.start();
    t2.start();
}
  • t1이 43을, t2가 44를 가져간다. lastIdUsed는 44이다(O)
  • t1이 44을, t2가 43를 가져간다. lastIdUsed는 44이다(O)
  • t1이 43을, t2가 43를 가져간다. lastIdUsed는 43이다(X)
  • 위와 같이 일부 경로가 잘못된 결과를 내놓는다

동시성 방어 원칙

단일 책임 원칙

  • 동시성과 관련된 코드는 다른 코드들과 분리한다
    - 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다
    • 동시성 코드에는 독자적인 문제가 있다
    • 잘못 구현된 동시성 코드는 다양한 방식으로 실패한다

자룔 번위를 제한하라

  • 공유 객체를 사용하는 코드 내 임계 영역을 synchronized 키워드로 보호한다
  • 자료를 캡슐화하고 공유 자료를 최대한 줄인다
  • 공유 자료를 수정하는 위치가 많을수록 위험도 커진다
    - 보호할 임계 영역을 빼먹는다
    • 모든 임계 영역을 제대로 보호했는지 확인하느라 노력과 수고가 든다
    • 버그를 찾기 어렵다

자료 사본을 사용하라

  • 처음부터 공유하지 않도록 한다
  • 객체를 복사해 읽기 전용으로 사용한다

스레드는 가능한 독립적으로 구현하라

  • 다른 스레드와 자료를 공유하지 않는다
  • 다른 스레드와 동기화 할 필요성을 만들지 않는다

라이브러리를 이해하라

  • 자바 5는 동시성 측면에서 발전했다
    - 스레드 환경에 안전한 컬렉션을 사용
    - 서로 무관한 작업을 수행한 때는 executor 프레임워크를 사용
    - 스레드가 차단되지 않는 방법을 사용
    - 일부 클래스 라이브러리는 스레드에 안전하지 못하다

스레드 환경에 안전한 컬렉션

  • java.util.concurrent 패키지

실행 모델을 이해하라

  • 기본 용어
    - 한정된 자원 : 다중 스레드 환경에서 사용하는 자원으로 크기나 숫자가 제한적이다. 데이터베이스 연결, 길이가 일정한 읽기/쓰기 버퍼 등
    - 상호 배제 : 한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우
    - 기아 : 한 스레드나 여러 스레드가 굉장히 오랫동안 혹은 영원히 자원을 기다린다
    - 데드락 : 여러 스레드가 서로 끝나기를 기다린다. 모든 스레드가 각기 필요한 자원을 다른 스레드가 점유하는 바람에 어느 쪽도 더 이상 진행하지 못한다
    - 라이브락 : 락을 거는 단계에서 각 스레드가 서로를 방해한다. 스레드는 계속해서 진행하려 하지만 공명으로 인해 굉장히 오랫동안 혹은 영원히 진행하지 못한다

생산자-소비자

  • 하나 이상 생산자 스레드가 정보를 생성해 버퍼나 대기열에 넣는다
  • 하나 이상의 소비자 스레드가 대기열에서 정보를 가져와 사용한다
  • 대기열은 한정된 자원이다
  • 대기열에 빈 공간이 있어야 정보를 채운다
  • 대기열에 정보가 있어야 가져올 수 있다
  • 생산자 스레드와 소비자 스레드는 서로에게 시그널을 보낸다
  • 잘못하면 둘 다 서로에게서 시그널을 기다릴 가능성이 존재한다

읽기-쓰기

  • 읽기 스레드를 위한 주된 정보원으로 공유 자원을 사용하지만 쓰기 스레드가 이 공유 자원을 이따금 갱신한다
  • 처리율의 문제가 핵심이다
  • 처리율을 강조하면 기아 현상이 생기거나 오래된 정보가 쌓인다
  • 갱신을 허용하면 처리율에 영향이 미친다
  • 대개는 쓰기 스레드가 버퍼를 오랫동안 점유하는 바람에 여러 읽기 스레드가 버퍼를 기다리느라 처리율이 떨어진다
  • 읽기 스레드가 없을 때까지 쓰기 스레드가 버퍼를 기다리는 방법을 쓰면 스레드가 기아 상태에 빠진다

식사하는 철학자들

  • 철학자를 스레드로 포크를 자원으로 바꿔 생각
  • 주의해서 설계하지 않으면 데드락, 라이브락, 처리율 저하, 효율성 저하 등을 겪을 수 있다

동기화하는 메서드 사이에 존재하는 의존성을 이해하라

  • 동기화하는 메서드 사이에 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 생긴다
  • 공유 객체 하나에는 메서드 하나만 사용해야 한다

동기화하는 부분을 작게 만들어라

  • 자바에서 synchronized 키워드를 사용하면 같은 락으로 감싼 모든 코드 영역은 한 번에 한 스레드만 실행이 가능하다
  • 락은 스레드를 지연시키고 부하를 가중시키므로 임계 영역 수를 최대한 줄여야 한다
  • 그러나 수를 줄인다고 필요 이상으로 임계 영역 크기를 키우면 스레드 간에 경쟁이 늘어나고 프로그램 성능이 떨어진다

올바른 종료 코드는 구현하기 어렵다

  • 깔끔하게 종료되는 코드는 올바로 구현하기 어렵다
  • 가장 흔히 발생하는 문제가 데드락이다. 즉 스레드가 절대 오지 않을 시그널을 기다린다
  • ex) 부모 스레드가 자식 스레드를 여러 개 만든 후 종료시키려는데 만약 자식 스레드 중 하나가 데드락에 걸렸다면 부모 스레드는 영원히 기다려야 한다

스레드 코드 테스트하기

  • 문제를 노출하는 테스트 케이스를 작성하라
  • 프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라
  • 테스트가 실패하면 원인을 추적하라

개인적인 감상

  • 동기화를 해야하는 순간들이 있겠지만 가능하다면 동기화를 피하고 자원을 공유하지 않는 것이 최선이라는 것을 알 수 있었다
  • 만약 동기화를 해야한다면 값을 그대로 사용하는 것보다 값을 복사해서 사용하는 것이 낫다는 것을 알 수 있었다

0개의 댓글