## 목차 ##
- 동시성이 필요한 이유?
- 난관
- 동시성 방어 원칙
- 라이브러리를 이해하라
- 실행 모델을 이해하라
- 동기화하는 메소드 사이에 존재하는 의존성을 이해하라
- 동기화하는 부분을 작게 만들어라
- 올바른 종료 코드는 구현하기 어렵다
- 스레드 코드 테스트하기
- 결론
## 동시성이 필요한 이유? ##
동시성은 '무엇'과 '언제'를 분리한다.
- 단일 스레드에서는 '무엇'과 '언제'가 밀접하다.
- 다중 스레드 세상에서는 프로그램이 작은 협력 프로그램 여러개로 보인다.
응답시간 및 작업 처리량(throughput) 개선을 위해서
- 여러 작업을 동시에 처리하므로 응답속도가 빨라진다.
동시성은 항상 성능을 높여준다?
- 여러 프로세서가 동시에 처리할 독립적인 계산과정이 충분히 많은 경우에는 그렇다.
동시성을 구현해도 설계는 변하지 않는다?
- 단일 스레드 시스템과 멀티 스레드 시스템은 설계가 판이하게 다르다.
동시성은 부하를 유발한다.
- 성능 측면에서 부하가 걸린다.
간단한 문제라도 동시성은 복잡하다.
동시성에서 기인하는 버그는 재현하기 어렵다.
- 일회성 문제라 여기고 무시하고 넘어가기 쉽다.
## 난관 ##
동시성을 구현하기 어려운 이유. 아래 코드를 보자.
public class X {
private int lastIdUsed;
public int getNextId() {
return ++lastIdUsed;
}
}
인스턴스 X를 생성하면서 lastIdUsed를 42로 초기화했다. thread1, thread2 두 스레드가 해당 객체의 getNextId()
를 호출하면 결과는 셋 중 하나다.
(a) thread1은 43을, thread2는 44를 받는다. lastIdUsed는 44가 된다.
(b) thread1은 44를, thread2는 43을 받는다. lastIdUsed는 44가 된다.
(c) thread1과 thread2가 모두 43을 받는다. lastIdUsed는 그대로 43이다.
(c)와같은 놀라운 결과가 발생할 수 있다. 두 스레드가 자바 코드 한 줄을 거쳐가는 경로는 수없이 많다. 이것을 이해하려면 JIT 컴파일러와 자바 메모리모델이 원자로 간주하는 최소단위를 알아야한다.
대부분의 경로는 (a)나 (b)와 같은 올바른 경로를 내놓지만 문제는 잘못된 결과를 내놓는 '일부' 경로다.
## 동시성 방어 원칙 ##
단일 책임 원칙(SRP)
- 동시성 코드는 복잡성 하나만으로 따로 분리될 이유가 충분하다.
- 동시성 관련 코드는 다른 코드에서 분리해야한다.
자료 범위를 제한하라
- 공유객체를 사용하는 임계영역(critical section) 코드를 synchronized 키워드로 보호한다.
- 임계영역의 수를 최소로 줄여라.
- 공유자료를 수정하는 위치가 많을수록 보호를 빼먹게되고 디버깅이 더욱 어려워진다.
- 공유자료를 캡슐화하한다.
자료 사본을 사용하라
- 객체를 복사해서 사용한다.
- 객체 복사에 드는 시간과 부하를 고려해 복사 비용을 실측해보고 결정한다.
- 객체를 복사함으로써 데드락을 피하는 이점이 사본 생성과 가비지 컬렉션 오버헤드 손실을 상쇄할 가능성이 크다.
스레드는 가능한 독립적으로 구현하라
- 가능하면 다른 스레드와 자료를 공유하지 않는다.
## 라이브러리를 이해하라 ##
스레드 환경에 안전한 컬렉션을 사용한다.
- java.util.concurrent 패키지
- ConcurrentHashMap은 Thread-safe하며 거의 모든 상황에서 HashMap보다 빠르다.
서로 무관한 작업을 수행할 때는 executor 프레임워크를 사용한다.
## 실행 모델을 이해하라 ##
생산자-소비자(Producer-Consumer) 모델
- 하나 이상의 생산자가 데이터를 생성해 buffer, queue와 같은 자료구조에 넣는다.
- 하나 이상의 소비자 스레드가 정보를 가져와 사용한다.
- 생산자 스레드가 정보를 채우면 신호를 보내 데이터가 채워졌음을 알린다.
- 소비자 스레드가 정보를 가져가면 신호를 보내 빈 공간이 있음을 알린다.
- 잘못하면 진행 가능함에도 불구하고 서로의 시그널을 기다릴 가능성이 있다.
읽기-쓰기
- 공유된 자원을 읽는 쓰레드와 쓰는 쓰레드가 동시에 존재한다.
- 공유자원이 이따금 갱신이 된다면 읽기/쓰기 스레드는 서로가 사용중일 때 기다려야한다.
- 한 쪽에 우선순위를 주기에는 다른 한 쪽이 기아상태에 빠질 가능성이 있다.
- 양쪽 균형을 잡는 해법이 필요하다.
## 동기화하는 메소드 사이에 존재하는 의존성을 이해하라 ##
- 웬만하면 공유객체를 여러 synchronized 메소드로 보호하지 말자.
- 불필요한 의존성이 생길 수 있다.
- 웬만하면 하나로 줄이자.
## 동기화하는 부분을 작게 만들어라 ##
- synchronized 키워드에 의한 락(lock)은 스레드를 지연시키고 부하를 가중시킨다.
- synchronized 키워드를 남발하지 말자.
- 임계영역은 반드시 보호해야한다.
- 따라서 임계영역 수를 최대한 줄여야한다.
## 올바른 종료 코드는 구현하기 어렵다 ##
- 데드락에 걸려 영원히 기다리는 현상 시 제대로 종료할 수 없게된다.
- 개발 초기부터 이를 고려해 개발한다.
- 어려우므로 이미 나온 알고리즘 사용을 검토한다.
## 스레드 코드 테스트하기 ##
문제를 노출하는 테스트 케이스를 작성하라.
- 여러가지 설정 및 부하를 바꿔가며 실행해봐라.
- 실패했던 테스트가 다시 돌렸더니 통과하더라는 이유로 넘어가지 마라.
말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라
- 멀티 스레드는 때때로 '말이 안 되는' 오류를 일으킨다.
- 스레드 코드에 잠입한 버그는 수천, 수만번에 한 번씩 드러나기도 한다.
- 실패를 재현하기 아주 어렵다.
- 일회성 문제는 존재하지 않는다.
멀티 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자.
- 스레드 환경과 스레드 환경 밖에서 생기는 버그를 동시에 디버깅하지마라.
- 순차 코드부터 제대로 돌리자.
상황에 맞게 스레드를 제어할 수 있도록 코드를 작성하라.
- 프로그램 처리율과 효율에 따라 스레드 수를 조율하는 코드
프로세서 수보다 많은 스레드를 돌려보라.
- 스레드 스와핑이 잦으면 임계영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉬워진다.
보조 코드를 활용해라.
- 스레드 버그를 재현하기 어려운 이유는 수천, 수만가지 코드실행 경로중에 극소수만 실패하기 때문이다.
sleep()
,wait()
,yield()
,priority()
와 같이 실행경로에 영향을 미치는 메소드를 추가해 실행해본다.- AOF, CGLIB, ASM등과 같은 도구를 사용하면 보조코드를 자동으로 추가할 수 있다.
## 결론 ##
멀티 스레딩은 매우 어렵다.
주의하지 않으면 매우 이해하기 어려운 버그를 만날 수 있다.
SRP를 준수해라.
동시성 오류를 일으키는 원인들에 대해 이해한다.
- 공유 자료 조작, 자원 풀 공유 등
코드 보호
- 임계영역 코드를 보호해라.(synchronized 키워드)
- 잠글 필요가 없는곳을 절대 잠그지 않는다.
- 잠긴 영역에서 다른 잠긴 영역을 호출하지 않는다.
- 공유하는 객체 수와 범위를 최대한 줄인다.
스레드에서는 알 수 없는 일회성 버그들이 많다.
- 많은 플랫폼에서 많은 설정 방식을 통해 계속해서 반복테스트 해야한다.
보조코드를 추가하면 버그가 드러날 가능성이 높아진다.
- 직접 구현, 자동화 기술 사용
'소프트웨어 공학 > 클린코드' 카테고리의 다른 글
(클린코드) Chapter14 - 점진적인 개선 (0) | 2020.05.16 |
---|---|
(클린코드) Chapter12 - 창발성 (0) | 2020.05.16 |
(클린코드) Chapter11 - 시스템 (0) | 2020.05.16 |
(클린코드) Chapter10 - 클래스 (0) | 2020.05.16 |
(클린코드) Chapter09 - 단위 테스트 (0) | 2020.05.16 |