클린코드 (~ 245p)

동시성

객체는 처리의 추상화다. 스레드는 일정의 추상화다.

  • 동시성은 결합을 없애는 전략
    • 무엇(What)과 언제(When)를 분리하는 전략
  • 동시성에 대한 일반적인 오해
    1. 동시성은 항상 성능을 높여준다.
      • 동시성은 때로 성능을 높여준다.
      • 대기 시간이 아주 길어 여러 스레드가 프로세서를 공유할 수 있거나
      • 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우
    2. 동시성을 구현해도 설계는 변하지 않는다.
      • 단일 스레드 시스템과 다중 스레드 시스템 설계는 판이하게 다르다.
  • 동시성에 대한 타당한 생각
    1. 동시성은 다소 부하를 유발한다.
    2. 동시성은 복잡하다.
    3. 일반적으로 동시성 버그는 재현하기 어렵다.
    4. 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.
  • 동시성 방어 원칙
    1. SRP
      • 동시성 관련 코드는 다른 코드와 분리해야 한다.
      • 1-1. 자료 범위를 제한하라
        • 임계영역을 활용
        • 공유 자원을 최대한 줄여라
        • 자료를 캡슐화 하라
      • 1-2. 자료 사본을 이용하라
        • 공유 자원을 줄이려면 처음부터 공유하지 않는 방법이 제일 좋다.
        • 복사하여 읽기 전용으로 사용 가능
      • 1-3. 스레드는 가능한 독립적으로 구현하라
        • 다른 스레드와 자료를 공유하지 않는다.
        • 각 스레드는 클라이언트 요청 하나를 처리할 뿐, 비공유 출처에서 가져오며 로컬 변수를 활용.
        • 독립적으로 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라.
    2. 라이브러리를 이해하라
      • 스레드 세이프 한 컬렉션을 사용하라.
      • 일부 클래스 라이브러리는 스레드 세이프 하지 않다.
      • java.util.concurrent 패키지가 제공하는 클래스는 다중 스레드 환경에서 사용해도 안전하며, 성능도 좋다.
      • 실제로 ConcurrentHashMap 은 거의 모든 상황에서 HashMap 보다 빠르다고 한다. (정말?)
    3. 실행 모델을 이해하라
      • Bound Resource(한정된 자원) : 다중 스레드 환경에서 사용하는 자원으로, 크기나 숫자가 제한적이다. DB 연결, 길이가 일정한 read/write buffere…
      • Mutual Exclusion(상호 배제) : 한 번에 한 스레드만 공유 자료/자원 을 사용할 수 있는 경우
      • Stravation(기아) : 오랫동안 혹은 영원히 자원을 기다린다.
      • Deadlock(데드락) : 여러 스레드가 서로가 끝나기를 기다린다.
      • Livelock(라이브락) : 락을 거는 단계에서 각 스레드가 서로를 방해한다. 스레드는 계속해서 진행하려 하지만, 굉장히 오랫동안 혹은 영원히 진행하지 못한다.
      • 생산자-소비자
        • 생산자는 대기열에 빈 공간이 있을 때 정보를 채우고, 소비자는 대기열에 정보가 있어야 가져온다.
        • 생산자와 소비자는 서로에게 시그널을 보낸다.
          • 생산자는 대기열에 정보를 채운 다음 “대기열에 정보가 있다.”
          • 소비자는 정보를 읽어들인 후 “대기열에 빈 공간이 있다.”
          • 잘못하면 생산자 스레드와 소비자 스레드가 둘 다 진행 가능함에도 불구하고 동시에 서로에게서 시그널을 기다릴 가능성이 존재한다.
      • 읽기-쓰기
        • 읽기 스레드를 위한 주된 정보원 공유 자원을 쓰기 스레드가 이따금 갱신한다고 하자.
        • 간단한 방법, 읽기 스레드가 요청이 없을 때 까지 쓰기 스레드가 기다린다.
          • 읽기 스레드가 계속 이어진다면 쓰기 스레드가 기아 상태에 빠진다.
        • 반면, 쓰기 스레드에게 우선권을 준 상태에서 쓰기 스레드가 계속 이어진다면 처리율이 떨어진다.
        • 양 쪽 균형을 잡으면서 동시 갱신 문제를 피하는 해법이 필요하다.
      • 식사하는 철학자들
        • 둥근 식탁에 철학자 한 무리가 둘러앉아 있고, 각 철학자 왼쪽에는 포크가 놓여있다. 식탁 가운데는 커다란 스파게티 한 접시가 놓여있다.
        • 철학자들은 배가 고프지 않으면 생각하며 시간을 보낸다.
        • 배가 고프면 양손에 포크를 집어들고 스파게티를 먹는다. (양손에 포크를 쥐지 않으면 먹지 못한다.) 먹고 나면 포크를 내려놓고 배가 고플 때까지 생각에 잠긴다.
        • 왼쪽 철학자나 오른쪽 철학자가 포크를 사용중이라면 포크를 내려놓을 때 까지 기다려야 먹을 수 있다.
        • 철학자를 스레드로, 포크를 자원으로 바꿔 생각하면 멀티스레드 문제이다.
    4. 동기화하는 메서드 사이에 존재하는 의존성을 이해하라
      • 공유 객체 하나에는 메서드 하나만 사용하라.
      • 공유 객체 하나에 여러 메서드가 필요한 상황일 경우 아래 세 가지 방법을 고려한다,
        1. 클라이언트에서 잠금
          • 클라이언트에서 첫 번째 메서드를 호출하기 전에 서버를 잠근다. 마지막 메서드를 호출할 때까지 잠금을 유지한다.
        2. 서버에서 잠금
          • 서버에다 “서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하는” 메서드를 구현한다. 클라이언트는 이 메서드를 호출한다.
        3. 연결(Adapted) 서버
          • 잠금을 수행하는 중간 단계를 생성한다. 서버에서 잠금 방식과 유사하지만 원래 서버는 변경하지 않는다.
    5. 동기화하는 부분을 작게 만들어라
      • 자바에서 synchronized 키워드를 사용하면 락을 설정한다. 같은 락으로 감싼 모든 코드 영역은 한 번에 한 스레드만 실행이 가능하다.
      • 락은 스레드를 지연시키고 부하를 가중시킨다.
      • 따라서 synchronized 문을 남발하는 코드는 바람직하지 않다.
      • 하지만 임계영역은 반드시 보호해야 한다.
      • 결국 임계영역 수를 최대한 줄여야 한다.
    6. 올바른 종료 코드는 구현하기 어렵다
      • 영구적으로 돌아가는 시스템을 구현하는 방법과 잠시 돌다 깔끔하게 종료하는 시스템을 구현하는 방법은 다르다.
      • 깔끔하게 종료하는 코드는 올바로 구현하기 어렵다. 가장 흔히 발생하는 문제가 데드락 이다. 스레드가 절대 오지 않을 시그널을 기다린다.
      • 종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라.
      • 생각보다 어려우므로 이미 나온 알고리즘을 검토하라.
    7. 스레드 코드 테스트하기
      • 테스트가 정확성을 완전히 보장하지는 않는다.
      • 그럼에도 충분한 테스트는 위험을 낮춘다.
        1. 말이 안되는 실패는 잠정적인 스레드 문제로 취급하라
          • 하지만 시스템 실패를 일회성이라 치부하지는 마라.
        2. 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
          • 스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅하지 마라.
          • 먼저 스레드 환경 밖에서 코드를 올바로 돌려라.
        3. 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 쓰레드 코드를 구현하라
          • 다중 스레드를 쓰는 코드를 다양한 설정으로 실행하기 쉽게 구현하라.
        4. 다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라
          • 적절한 스레드 개수를 파악하려면 상당한 시행착오가 필요하다.
          • 다양한 설정으로 프로그램의 성능 측정 방법을 강구한다.
        5. 프로세서 수보다 많은 스레드를 돌려보라
        6. 다른 플랫폼에서 돌려보라
        7. 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라
  • 다중 스레드 코드는 올바로 구현하기 어렵다. 간단했던 코드가 여러 스레드와 공유 자료를 추가하면서 악몽으로 변한다. 각별히 깨끗하게 코드를 짜야 한다.