[Clean Code] 13. 동시성

  images cleancode

  • 클린 코드 책을 읽으며 내용을 요약 정리한 글입니다.

13. 동시성

동시성이 필요한 이유?

  • 동시성은 결합(Coupling)을 없애는 전략이다. 즉, 무엇(What)과 언제(When)를 분리하는 전략이다.
  • 무엇과 언제를 분리하면 애플리케이션 구조와 효율이 극적으로 나아진다.
  • 구조적 개선만을 위해 동시성을 채택하는 건 아니다. 어떤 시스템은 응답 시간과 작업 처리량(throughput) 개선이라는 요구사항으로 인해 직접적인 동시성 구현이 불가피하다.

    • 예시

      • 한 번에 한 사이트를 방문하는 대신 다중 스레드 알고리즘을 이용하면 수집기 성능을 높일 수 있다.
      • 많은 사용자를 동시에 처리하면 시스템 응답 시간을 높일 수 있다.
      • 정보를 나눠 여러 컴퓨터에서 돌리는 경우, 대량의 정보를 병렬로 처리하는 경우

미신과 오해

  • 미신과 오해

    • 동시성은 항상 성능을 높여준다.

      • 동시성은 때로 성능을 높여준다.
      • 대기 시간이 아주 길어 여러 스레드가 프로세서를 공유할 수 있거나, 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 성능이 높아진다.
    • 동시성을 구현해도 설계는 변하지 않는다.

      • 단일 스레드 시스템과 다중 스레드 시스템은 설계가 판이하게 다르다. 일반적으로 무엇과 언제를 분리하면 시스템 구조가 크게 달라진다.
  • 타당한 생각

    • 동시성은 다소 부하를 유발한다.

      • 성능 측면에서 부하가 걸리며, 코드도 더 짜야 한다.
    • 동시성은 복잡하다.
    • 일반적으로 동시성 버그는 재현하기 어렵다.
    • 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.

난관

public class X { 
  private int lastIdUsed;
  
  public int getNextId() {
    return ++lastIdUsed;
  }
}
  • 인스턴스 X를 생성하고 lastIdUsed 필드를 42로 설정한 다음, 두 스레드가 해당 인스턴스를 공유한다.
  • getNextId(); 를 호출하면 결과는 아래 셋중 하나이다.

    • 1번 스레드는 43을 받고 2번 스레드는 44를 받는다. (lastIdUsed = 44)
    • 1번 스레드는 44를 받고 2번 스레드는 43을 받는다. (lastIdUsed = 44)
    • 1번 스레드, 2번 스레드 모두 43을 받는다. (lastIdUsed = 43)
  • 두 스레드가 같은 변수를 동시에 참조하면 일부 경로에서 세 번째와 같은 잘못된 결과를 내놓는다.

동시성 방어 원칙

단일 책임 원칙 (Single Responsibility Principle, SRP)

  • SRP : 주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 한다는 원칙
  • 권장 사항 : 동시성 관련 코드는 다른 코드와 분리해야 한다.

따름 정리(Colollary) : 자료 범위를 제한하라

  • 공유 객체를 사용하는 코드 내 임계영역(critical section)을 synchronized 키워드로 보호하라고 권장한다.
  • 이런 임계영역의 수를 줄이는 기술이 중요하다.
  • 권장 사항 : 자료를 캡슐화(encapsulation)하라. 공유 자료를 최대한 줄여라.

따름 정리 : 자료 사본을 사용하라

  • 공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 제일 좋다.
  • 어떤 경우에는 각 스레드가 객체를 복사해 사용한 후 한 스레드가 해당 사본에서 결과를 가져오는 방법도 가능하다.
  • 동기화 비용 > 객체 복사 비용 일 수 있다.

따름 정리 : 스레드는 가능한 독립적으로 구현하라

  • 다른 스레드와 자료를 공유하지 않는다.
  • 권장 사항 : 독자적인 스레드로, 가능하면 다른 프로세서에서, 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라.

실행 모델을 이해하라

  • 다중 스레드 프로그래밍에서 사용하는 실행 모델 몇 가지

    • 생산자-소비자(Producer-Consumer)

      • 생산자 스레드가 정보를 생성해 Buffer나 Queue에 넣는다.
      • 소비자 스레드가 Queue에서 정보를 가져와 사용한다.
      • Queue는 한정된 자원이다.
    • 읽기-쓰기(Readers-Writers)

      • 읽기 스레드의 요구와 쓰기 스레드의 요구를 적절히 만족시켜 처리율도 적당히 높이고 기아도 방지하는 해법이 필요하다.
    • 식사하는 철학자들(Dining Philosophers)

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

  • 공유 클래스 하나에 동기화된 메서드가 여럿이라면 구현이 올바른지 다시 한 번 확인해야 한다.
  • 권장사항 : 공유 객체 하나에는 메서드 하나만 사용하라.
  • 공유 객체 하나에 여러 메서드가 필요한 상황에서는 다음 세 가지 방법을 고려한다.

    • 클라이언트에서 잠금

      • 클라이언트에서 첫 번째 메서드를 호출하기 전에 서버를 잠그고 마지막 메서드를 호출할 때까지 잠금을 유지한다.
    • 서버에서 잠금

      • 서버에 서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하는 메서드를 구현한다. 클라이언트는 이 메서드를 호출한다.
    • 연결(Adapted) 서버

      • 잠금을 수행하는 중간 단계를 생성한다. 서버에서 잠금 방식과 유사하지만 원래 서버는 변경하지 않는다.

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

  • 코드를 짤 때는 임계영역 수를 최대한 줄여야 한다.
  • 권장 사항 : 동기화하는 부분을 최대한 작게 만들어라.

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

  • 가장 흔히 발생하는 문제가 데드락이다. 스레드가 절대 오지 않을 시그널을 기다린다.
  • 권장 사항 : 종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라. 생각보다 오래 걸린다. 생각보다 어려우므로 이미 나온 알고리즘을 검토하라.

스레드 코드 테스트하기

  • 권장 사항 : 문제를 노출하는 테스트케이스를 작성하라. 프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라. 테스트가 실패하면 원인을 추적하라. 다시 돌렸더니 통과하더라는 이유로 그냥 넘어가면 절대로 안 된다.

말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라

  • 권장 사항 : 시스템 실패를 ‘일회성’이라 치부하지 마라.

다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자

  • 권장 사항 : 스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅하지 마라. 먼저 스레드 환경 밖에서 코드를 올바로 돌려라.

다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드를 구현하라

  • 한 스레드로 실행하거나, 여러 스레드로 실행하거나, 실행 중 스레드 수를 바꿔본다.
  • 스레드 코드를 실제 환경이나 테스트 환경에서 돌려본다.
  • 테스트 코드를 빨리, 천천히, 다양한 속도로 돌려본다.
  • 반복 테스트가 가능하도록 테스트 케이스를 작성한다.
  • 권장 사항 : 다양한 설정에서 실행할 목적으로 다른 환경에 쉽게 끼워 넣을 수 있게 코드를 구현하라.

다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라

  • 스레드 개수를 조율하기 쉽게 코드를 구현한다.
  • 프로그램 처리율과 효율에 따라 스스로 스레드 개수를 조율하는 코드도 고민한다.

프로세서 수보다 많은 스레드를 돌려보라

  • 시스템이 스레드를 Swapping 할 때도 문제가 발생한다. 스와핑을 일으키려면 프로세서 수보다 많은 스레드를 돌린다.

다른 프로그램에서 돌려보라

  • 권장 사항 : 처음부터 그리고 자주 모든 목표 플랫폼에서 코드를 돌려라.

코드에 보조 코드(instrunment)를 넣어 돌려라. 강제로 실패를 일으키게 해보라

  • 보조 코드를 추가해 코드가 실행되는 순서를 바꿔준다.
  • 직접 구현하기
  • 자동화

결론

  • SRP를 준수한다.
  • 동시성 오류를 일으키는 잠정적인 원인을 철저히 이해한다.
  • 사용하는 라이브러리와 기본 알고리즘을 이해한다.
  • 보호할 코드 영역을 찾아내는 방법과 특정 영역을 잠그는 방법을 이해한다.
  • 스레드 코드는 많은 플랫폼에서 많은 설정으로 반복해서 계속 테스트해야 한다.

Written by@Ji Yeong Choe
안녕하세요 !

GitHub