객체의 처리는 추상화다. 스레드는 일정의 추상화다.
-
동시성이 필요한 이유 ?
-
동시성은 커플링 ( 결합 ) 을 없애는 전략이다.
-
즉, 무엇과 언제를 분리하는 전략.
-
쓰레드가 하나인 프로그램은 무엇과 언제가 서로 밀접하다.
-
호출 스택을 살펴보면 프로그램 상태가 곧바로 드러난다.
-
이 무엇과 언제를 분리하면 애플리케이션 구조와 효율이 극적으로 나아진다.
-
동시성은 때로 성능을 높여준다.
- 대기시간이 아주 길어 여러 쓰레드가 프로세서를 공유할 수 있거나, 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 성능이 높아진다.
- 어느쪽도 일상적으로 발생하는 상황은 아니다.
-
단일 스레드와 멀티 스레드 시스템은 설계가 판이하게 다르다.
- 일반적으로 무엇과 언제를 분리하면 시스템 구조가 크게 달라진다.
-
실제로 컨테이너가 어떻게 동작하는지, 어떻게 동시 수정, 데드락 등과 같은 문제를 피할 수 있는지를 알아야만 한다.
-
동시성은 다소 부하를 유발한다.
- 성능 측면에서 부하가 걸리며 코드도 더 짜야 한다.
-
동시성은 복잡하다.
- 간단한 문제라도 동시성은 복잡하다.
-
일반적으로 동시성 버그는 재현하기 어렵다.
- 그래서 진짜 결함으로 간주되지 않고 일회성 문제로 여겨 무시하기 쉽다.
-
동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.
-
-
난관
여러 쓰레드가 같은 객체의 필드를 참조할 경우 필드가 쓰레드에 의해 값이 계속 바뀐다.
-
동시성 방어 원칙
-
단일 책임 원칙
- 동시성 코드는 독자적인 개발,변경,조율 주기가 있다.
- 동시성 코드에는 독자적인 난관이 있다.
- 다른 코드에서 겪는 난관과 다르며 훨씬 어렵다.
- 잘못 구현한 동시성 코드는 별의별 방식으로 실패한다.
- 주변에 있는 다른 코드가 발목을 잡지 않더라도 동시성 하나만으로도 충분히 어렵다.
- 동시성 코드는 다른 코드와 분리하라.
-
따름 정리: 자료 범위를 제한하라.
-
난관에서 나온 것 처럼 두 스레드가 하나의 객체에 필드를 건드리게 되므로 예상치 못한 결과를 낳는다.
-
이런 문제를 해결하기 위해서는 공유 객체를 사용하는 코드 내 임계영역을 synchronized 키워드로 보호하라고 권장한다.
-
이런 임계영역의 수를 줄이는 기술이 중요하다.
-
임계 영역이 많을 때 단점
- 보호할 임계영역을 빼먹으면 공유자료를 수정하는 모든 코드가 망가진다.
- 모든 임계영역을 올바로 보호했는지 ( DRY 위반 ) 을 확인하느라 똑같은 노력과 수고를 반복한다.
- 버그가 찾기 더 어려워진다.
- 자료를 캡슐화 하여 공유 자료를 최대한 줄여라.
-
-
따름정리 : 자료 사본을 사용하라.
-
공유 자료를 처음 부터 공유하지 않으면 공유 자료를 줄일 수 있다.
-
객체를 복사해 읽기 전용으로 사용해도 줄일수 있다.
-
각 스레드가 객체를 복사해 사용 한 후 한 스레드가 해당 사본에서 결과를 가져오는 방법도 가능.
-
복사의 비용과 시간이 걱정스러울 수도 있다.
- 복사비용이 진짜 문제인지 실측 해보도록 한다.
- 하지만 사본으로 동기화 를 피할 수 있다면 내부 잠금 ( LOCK ) 을 없애 절약한 수행시간이 사본 생성과 카비지 컬렉션에 드는 부하를 상쇄할 가능성이 크다.
-
-
따름정리 : 쓰레드는 가능한 독립적으로 구현하라
-
다른 스레드와 자료를 공유 하지 않는 독립적인 스레드를 구현한다.
-
각 스레드는 클라이언트 요청 하나를 처리
-
모든 정보는 비공유 출처에서 가져오며 로컬변수에 저장.
-
그러면 각 스레드는 각각 독립적이게 된다.
-
HttpServlet 클래스에서 파생한 클래스는 모든 정보를 doGet과 doPost 매개변수로 받는다.
-
각 서블릿은 마치 자신이 독자적인 시스템에서 동작하는 양 요청을 처리한다.
-
물론 서블릿을 사용하는 대다수 프로그램은 데이터베이스 연결과 같은 자원을 공유하는 상황에 처한다.
-
권장사항
- 독자적인 스레드로 가능하면 다른 프로세서에서 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라.
-
-
- 쓰레드 환경에 안전한 컬렉션을 사용한다. ( 자바 5부터 제공)
- 서로 무관한 작업을 수행할 떄는 executor 프레임워크를 사용한다.
- 가능하다면 스레드가 blocking 되지 않는 방법을 사용한다.
- 일부 클래스 라이브러리는 스레드에 안전하지 못하다.
이사람이 스레드에 사용해도 안전한 컬렉션 클래스 몇개를 구현햇는데
나중에 이게 java.util.concurrent 패키지에 추가됬다.
-
java.util.concurrent 패키지가 제공하는 클래스는 다중 스레드 환경에서 사용해도 안전
-
ReentrantLock
- 한 메서드에서 잠그고 다른메서드에서 푸는 락 ( Lock ) 이다.
-
Semaphore
- 전형적인 세마포어다. 개수 ( Count ) 가 있는 락이다.
-
CountDownLatch
- 지정한 수 만큼 이벤트가 발생하고 나서야 대기중인 쓰레드를 모두 해제하는 락이다.
- 모든 스레드에게 동시에 공평하게 시작할 기회를 준다.
-
java.util.concurrent
-
java.util.concurrent.atomic
-
java.util.concurrent.locks
-
위에 3가지 를 익혀라.
- Bound Resources
- 다중 스레드 환경에서 사용하는 자원으로 크기나 숫자가 제한적이다.
- 데이터베이스 연결, 길이가 일정한 읽기/쓰기 버퍼 등이 예다.
- Mutual Exclusion
- 한번에 한 쓰레드만 공유 자료나 공유 자원을 사용할 수 있는 경우를 가리킨다.
- Starvation
- 한 스레드나 여러 스레드가 굉잦ㅇ히 오랫동안 혹은 영원히 자원을 기다린다.
- 예를 들어 항상 짧은 스레드에게 우선순위를 준다면 짧은 스레드가 지속적으로 이어질 경우 긴 스레드가 기아 상태에 빠진다.
- Deadlock
- 여러 스레드가 서로가 끝나기를 기다린다.
- 모든 스레드가 각기 필요한 자원을 다른 스레드가 점유하는 바람에 어느 쪽도 더이상 진행하지 못한다.
- Livelock
- 락을 거는 단계에서 각 스레드가 서로를 방해한다.
- 스레드는 계속해서 진행하려 하지만, 공명( Resonance ) 으로 인해 굉장히 오랫동안 혹은 영원히 진행하지 못한다.
-
하나 이상 생산자 스레드가 정보를 생성해 버퍼 ( Buffer ) 나 대기열 ( Queue )에 넣는다.
-
하나 이상 소비자 스레드가 대기열에서 정보를 가져와 사용한다.
-
생산자 스레드와 소비자 스레드가 사용하는 대기열은 한정된 자원 이다.
-
생산자 스레드는 대기열에 빈 공간이 있어야 정보를 채운다.
- 즉 빈공간이 생길때까지 기다린다.
-
소비자 스레드는 대기열에 정보가 있어야 가져온다.
- 즉 정보가 채워질 때 까지 기다린다.
-
대기열을 올바로 사용하고자 두 스레드는 서로에게 신호를 보낸다.
-
생산자 스레드는 대기열에 정보를 채우고 소비자 스레드에 신호를 보낸다.
-
소비자 스레드는 대기열 정보를 읽고 빈 공간이 있다는 시그널을 보낸다.
-
잘못하면 생산자 스레드 , 소비자 스레드 둘다 진행 가능함에도 불구하고
-
동시에 서로에게서 시그널을 기다릴 가능성이 존재한다.
-
생산자 스레드는 빈 공간이 있어야 정보를 채우기 때문에 하나만 넣고 신호를 보내면
-
소비자 스레드는 빈 공간이 있다는 정보를 다시 생산자한테 보낸다.
-
그래서 둘다 기다리는 상태가 될 수가 있다.
-
읽기 쓰레드를 위한 주된 정보원으로 공유 자원을 사용하지만, 쓰기 스레드가 이 공유 자원을 이따금 갱신한다고 하자, 이런 경우 처리율 ( throughtput ) 이 문제의 핵심이다.
-
처리율을 강조하면 기아 ( starvation ) 현상이 생기거나 오래된 정보가 쌓인다.
-
갱신을 허용하면 처리율에 영향을 미친다.
-
쓰기 쓰레드가 버퍼를 갱신하는 동안 읽기 쓰레드가 버퍼를 읽지 않으려면 반대로 읽기 쓰레드가 읽는동안 쓰기 쓰레드가 버퍼를 갱신하지 않으려면 복잡한 균형잡기가 필요하다.
-
대개는 쓰기 쓰레드가 버퍼를 오랫동안 점유하기 떄문에 여러 읽기스레드가 버퍼를 기다리느라 처리율이 떨어진다.
-
따라서 두 쓰레드의 요구를 적절히 만족시켜 처리율도 적당히 높이고 기아도 방지하는 해법이 필요.
-
간단한 전략은 읽기 스레드가 없을 떄 까지 갱신을 원하는 쓰기 쓰레드가 버퍼를 기다리는 방법.
- 하지만 읽기 스레드가 계속 이어진다면 쓰기 스레드는 기아 상태에 빠진다.
- 반면 쓰기스레드에게 우선권을 준 상태에서 쓰기 쓰레드가 계속 이어진다면 처리율이 떨어진다.
-
양쪽균형을 잡으면서 동시에 갱신문제를 피하는 해법이 필요하다.
- 기업 애플리케이션은 여러 프로세스가 자원을 얻으려 경쟁한다.
- 주의해서 설계하지않으면 데드락, 라이브락, 처리율 저하, 효율성 저하 등을 겪는다.
-
동기화하는 메서드 사이에 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 생긴다.
-
자바 언어는 개별 메서드를 보호하는 synchronized라는 개념을 지원.
-
하지만 공유 클래스 하나에 동기화된 메서드가 여럿이라면 구현이 올바른지 다시한번 확인하라.
-
공유 객체 하나에는 메서드 하나만 사용하자.
- 클라이언트에서 잠금
- 클라이언트에서 첫번째 메서드를 호출하기 전에 서버를 잠근다. 마지막 메서드를 호출할 때 까지 잠금을 유지한다.
- 서버에서 잠금
- 서버에다 "서버를 잠그고 모든 메서드를 호출 한 후 잠금을 해제하는" 메서드를 구현한다. 이
- 연결( adapted ) 서버
- 잠금을 수행하는 중간 단계를 생성한다.
- "서버에서 잠금" 방식과 유사하지만 원래 서버는 변경하지 않는다.
-
자바에서 synchronized 키워드를 사용하면 락을 설정한다.
-
같응ㄴ 락으로 감싼 모든 영역은 한번에 한 스레드만 실행이 가능하다.
-
락은 스레드를 지연시키고 부하를 가중시킨다.
-
그러므로 synchronized문을 남발하지 말아야 한다.
-
반면 임계영역 (동시에 사용하는 부분 ) 은 반드시 보호해야한다.
-
코드를 짤때 임계영역은 최소로 줄이자.
-
동기화 하는 부분을 최대한 작게 만들어라
-
영구적으로 돌아가는 시스템을 구현하는 방법과 잠시 돌다 깔끔하게 종료하는 시스템을 구현하는 방법은 다르다.
-
흔히 발생하는 문제는 데드락.
-
깔끔하게 종료하는 다중스레드 코드를 짜야 한다면 시간을 투자해 올바로 구현하자.
-
종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라
-
생각보다 오래 걸린다. 생각보다 어려우므로 이미 나온 알고리즘을 검토하라
-
테스트가 정확성을 보장하지는 않는다.
-
그럼에도 테스트는 위험을 낮춰준다.
-
스레드가 하나인 프로그램은 위험을 낮춰주지만 같은 코드와 같은 자원을 사용하는 스레드가 둘 이상으로 늘어나면 상황은 급격하게 복잡해진다.
-
문제를 노출하는 테스트 케이스를 작성하라.
-
프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라.
-
테스트가 실패하면 원인을 추적하라. 다시 돌렸더니 통과하더라는 이유로 넘어가면 절대 안된다.
-
고려할 사항이 매우 많다. 아래 구체적인 지침을 제시한다.
- 말이 안되는 실패는 잠정적인 스레드 문제로 취급하라.
- 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자.
- 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있도록 스레드 코드를 구현하라.
- 다중 스레드를 쓰는 코드 부분을 상황에 맞춰 조정할 수 있게 작성하라.
- 프로세서 수보다 많은 스레드를 돌려보라.
- 다른 플랫폼에서 돌려보라.
- 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라.
-
다중 스레드 코드느 때때로 말이 안되는 오류를 일으킨다.
-
실패를 재현하기가 아주 어렵다.
-
일회성 문제를 계속 무시한다면 잘못된 코드위에 코드가 계속 쌓인다.
-
시스템 실패를 일회성이라 치부하지마라
-
당연한 소리.
-
스레드 환경 밖에서 코드가 제대로 도는지 반드시 확인한다.
-
스레드가 호출하는 POJO를 만들면 스레드 환경밖에서 테스트가 가능하다.
-
POJO에 넣는 코드는 많을수록 좋다.
-
스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅하지마라
-
먼저 스레드 환경밖에서 코드를 올바르게 돌려보자
-
다중 스레드를 쓰는 코드는 다양한 설정으로 실행하기 쉽게 구현하자.
-
한 스레드로 실행하거나, 여러 스레드로 실행하거나, 실행 중 스레드 수를 바꿔본다.
-
스레드 코드를 실제 환경이나 테스트 환경에서 돌려본다.
-
테스트 코드를 빨리, 천천히, 다양한 속도로 돌려본다.
-
반복 테스트가 가능하도록 테스트 케이스를 작성한다.
-
다양한 설정에서 실행할 목적으로 다른 환경에 쉽게 끼워 넣을 수 있게 코드를 구현하라
- 적절한 스레드 개수를 파악하려면 상당한 시행착오가 필요하다.
- 스레드개수를 조율하기 쉽게 코드를 구현한다.
- 프로그램이 돌아가는 도중에 스레드 개수를 변경하는 방법도 고려한다.
- 프로그램 처리율과 효율에 따라 스스로 스레드 개수를 조율하는 코드도 고민한다.
- 시스템이 스레드를 스와핑할 떄도 문제가 발생한다.
- 스와핑을 일으키려면 프로세서 수보다 많은 스레드를 돌린다.
- 스와핑이 잦을수록 임계영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉬워진다.
- 윈도우에서 돌아가도 OS X에서는 안될수 있다.
-
흔히 스레드 코드는 오류를 찾기가 쉽지 않다.
-
오류가 엄청난 오랜시간이 지난뒤에 드러날수도 있다.
-
수천가지 경로중에 아주 소수만 실패하기 때문이다.
-
보조 코드를 추가해 코드가 실행되는 순서를 바꿔준다.
-
예를들어 Object.wait(), Object.sleep(), Object.yield(), Object.priority() 등과 같은 메서드를 추가해 코드를 다양한 순서로 실행한다.
-
각 메서드는 스레드가 실행되는 순서에 영향을 미친다. 따라서 버그가 드러날 가능성도 높아진다.
-
잘못된 코드라면 초반에 그리고 가능한 자주 실패하는 편이 좋다.
-
코드에 보조코드를 추가하는 방법은 두가지다.
- 직접 구현하기
- 자동화
-
직접 구현해서 추가했는데 에러가 났다면 어차피 잘못된 코드인데 증거가 드러난것이다.
- 항상 맞는말은 아니다.
- JVM은 선점형 스레드 스케줄링을 지원하지 않는다.
- 따라서 선점형 스케줄링을 지원하지 않는 OS에서 코드가 문제없이 돌아갈지도 모른다.
- 이유는 다르지만 반대상황 역시 가능하다.
-
이 방법에는 몇가지 문제가 있다.
- 보조코드를 삽입할 적정 위치를 직접 찾아야한다.
- 어떤 함수를 어디서 호출해야 적당한지 어떻게 알까 ?
- 배포 환경에 보조 코드를 그대로 남겨두면 프로그램 성능이 떨어진다.
- 무작위적이다. 오류가 드러날지도 모르고 드러나지 않을지도 모른다. 사실 안드러날 확률이 높다.
-
배포 환경이 아닌 테스트 환경에서 보조 코드를 실행할 방법이 필요하다.
-
실행할 떄마다 설정을 바꿔줄 방법도 필요하다.
-
그래야 전체적으로 오류가 드러날 확률이 높아진다.
-
스레드를 전혀 모르는pojo와 스레드를 제어하는 클래스로 프로그램을 분할하면 보조코드를 추가할 위치를 찾기가 쉬워진다.
-
게다가 여러상황에서 sleep,yield등으로 pojo를 호출하게 다양한 테스트 지그 ( jig ) 를 구현할 수도 있다.
-
보조 코드를 자동으로 추가하려면 AOF ( Aspect-Oriented Framework ) , CGLIB , ASM등과 같은 도구를 사용한다.
-
스레드 코드 테스트를 도와주는 도구중에 IBM이 개발한 ConTest라는 도구가 있다.
-
코드를 다양하게 삽입해서 실행 ( 흔들기 ) 하는 이유는 매번 다른 순서로 실행하기 위해서도
-
좋은 테스트 케이스와 흔들기 기법은 오류가 드러날 확률을 크게 높여준다.
-
흔들기 기법을 사용해 오류를 찾아내라
결론
-
SRP를 준수하자.
-
POJO를 사용해 스레드를 아는 코드와 스레드를 모르는 코드를 분리하자.
-
스레드 코드를 테스트할때는 전적으로 스레드만 테스트한다.
-
즉 스레드 코드는 최대한 집약되고 작아야 한다는 의미다.
-
동시성 오류가 나는 잠정적인 원인을 철저히 이해한다.
-
사용하는 라이브러리와 기본 알고리즘을 이해한다.
-
특정 라이브러리 기능이 기본 알고리즘과 유사한 어떤 문제를 어떻게 해결하는지 파악한다.
-
보호할 코드 영역을 찾아내는 방법과 특정 코드 영역을 잠그는 방법을 이해한다.
-
잠글 필요가 없는 코드는 잠그지 않는다.
-
그러려면 공유하는 정보와 공유하지 않는 정보를 제대로 이해해야 한다.
-
공유하는 객체 수와 범위를 최대한 줄인다.
-
클라이언트에게 공유 상태를 관리하는 책임을 떠넘기지 않는다.
-
필요하다면 객체 설계를 변경해 클라이언트에게 편의를 제공한다.
-
일회성문제를 가볍게 치부하고 넘기지 말라 나중에 부채가 되서 돌아온다.
-
스레드 코드는 많은 플랫폼에서 테스트해봐야 한다.
-
테스트 용이성은 TDD 3대규칙을 따르면 자연히 얻는다.
-
시간을 들여 보조 코드를 추가하면 오류가 드러날 가능성이 높아진다.
-
직접 구현이나 자동화기술을 사용하자.
-
초반부터 보조 코드를 고려해야 한다.
-
스레드 코드는 출시하기 전까지 최대한 오랫동안 돌려봐야 한다.
-
깔끔한 접근방식을 취한다면 코드가 올바로 돌아갈 가능성이 극적으로 높아진다.
-
- 더그 리라는 사람이 cuncurrent programing in java란 책을 집필했다.