객체는 처리의 추상화다. 스레드는 일정의 추상화다.
Why Concurrency?
동시성은 결합(Coupling)을 없애는 전략이다.
무엇과 언제를 분리하는 전략이다.
Myths and Misconceptions
동시성은 항상 성능을 높여준다.
-> 동시성은 때로 성능을 높여준다. 대기 시간이 아주 길어 여러 스레드가 프로세서를 공유할 수 있거나, 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 성능이 높아진다.
동시성을 구현해도 설계는 변하지 않는다.
-> 단일 스레드 시스템과 다중 스레드 시스템은 설계가 판이하게 다르다. 무엇과 언제를 분리하면 시스템 구조가 크게 달라진다.
웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다.
-> 컨테이너가 어떻게 동작하는지, 어떻게 동시 수정, 데드락 등과 같은 문제를 피할 수 있는지를 알아야만 한다.
동시성은 부하를 유발하고, 복잡하며, 동시성 버그는 재현하기 어렵다.
Challenges
public class X {
private int lastIdUsed;
public int getNextId() {
return ++lastIdUsed;
}
}
인스턴스 X를 생성하고, lastIdUsed 필드를 설정한 다음, 두 스레드가 해당 인스턴스를 공유한다고 치면, 그 중에서 일부경로가 잘못된 결과를 내놓을 수 있다.
Concurrency Defense Principles
동시성 코드가 일으키는 문제로부터 시스템을 방어하는 원칙과 기술
Single Responsibility Principle
SRP는 주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 한다는 원칙이다.
동시성 관련 코드는 다른 코드와 분리해야 한다는 뜻이다.
동시성 코드는 다른 코드와 분리하라.
Corollary: Limit the Scope of Data
두 스레드가 서로 간섭하는 문제를 해결하는 방안으로 공유 객체를 사용하는 코드 내 임계 영역을 synchronized 키워드로 보호하라고 권장한다.
자료를 캡슐화(Encapsulation)하라. 공유 자료를 최대한 줄여라.
Corollary: Use Copies of Data
공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 제일 좋고, 어떤 경우에는 객체를 복사해 읽기 전용으로 사용하는 방법이 있다.
Corollary: Threads Should Be as Independent as Possible
다른 스레드와 자료를 공유하지 않는다.
각 스레드는 클라이언트 요청 하나를 처리한다. 모든 정보는 비공유 출처에서 가져오며 로컬 변수에 저장한다.
독자적인 스레드로, 가능하면 다른 프로세서에서, 돌려도 괜찮도록 자료를 독립적인 다위로 분할하라.
Know Your Library
Thread-Safe Collections
ConcurrentHashMap은 거의 모든 상황에서 HashMap보다 빠르고, 동시 읽기/쓰기를 지원하며 자주 사용하는 복합 연산을 다중 스레드 상에서 안전하게 만든 메서드로 제공한다.
언어가 제공하는 클래스를 검토하라.
자바에서는 java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.locks가 있다.
Know Your Execution Models
한정된 자원(Bound Resource)
상호 배제(Mutual Exclusion)
기아(Starvation)
데드락(Deadlock)
라이브락(Livelock)
Producer-Consumer
생산자 스레드와 소비자 스레드가 사용하는 대기열은 한정된 자원이다.
생산자 스레드는 대기열에 빈 공간이 있어야 정보를 채우고, 소비자 스레드는 대기열에 정보가 있어야 가져온다.
즉, 생산자 스레드는 빈 공간이 생길 때까지 기다리고, 소비자 스레드는 정보가 채워질 때까지 기다린다.
대기열을 올바로 사용하고자 서로에게 시그널을 보내는데, 잘못하면 동시에 서로에게서 시그널을 기다릴 가능성이 존재한다.
Readers-Writers
읽기 스레드가 주로 공유 자원을 사용하지만, 쓰기 스레드가 이따금 공유 자원을 갱신한다.
읽기 스레드와 쓰기 스레드의 요구를 적절히 만족시켜 처리율을 적당히 높이고 기아도 방지하는 해법이 필요하다.
간단한 전략으로 읽기 스레드가 없을 때까지 갱신을 원하는 쓰기 스레드가 버퍼를 기다리는 방법이 있다.
하지만, 읽기 스레드가 계속 이어지면 쓰기 스레드는 기아 상태에 빠진다.
Dining Philosophers
애플리케이션을 여러 프로세스가 자원을 얻으려 경쟁한다.
주의해서 설계하지 않으면 데드락, 라이브락, 처리율 저하, 효율성 저하 등을 겪는다.
위의 기본 알고리즘과 각 해법을 이해하라
Beware Dependencies Between Synchronized Methods
동기화하는 메서드 사이에 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 생긴다.
공유 클래스 하나에 동기화된 메서드가 여럿이라면 구현이 올바른지 다시 한 번 확인하기 바란다.
공유 객체 하나에는 메서드 하나만 사용하라
공유 객체 하나에 여러 메서드가 필요할 때 고려사항
Keep Synchronized Sections Small
자바에서 synchronized 키워드를 사용하면 락을 설정한다.
그러나 락은 스레드를 지연시키고 부하를 가중시키므로 남발은 바람직하지 않다.
반면, 임계영역은 반드시 보호해야 한다.
따라서 임계영역 수를 최대한 줄여야 하는데, 이 때 거대한 임계영역 하나로 구현하는 것은 스레드 간에 경쟁이 늘어나고 프로그램 성능을 떨어뜨린다.
동기화하는 부분을 최대한 작게 만들어라
Writing Correct Shut-Down Code Is Hard
영구적으로 돌아가는 시스템을 구현하는 방법과 잠시 돌다 깔끔하게 종료하는 시스템을 구현하는 방법은 다르다.
깔끔하게 종료하는 다중 스레드 코드를 짜야한다면 데드락에 유의하고 시간을 투자해 올바로 구현해야 한다.
종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라.
생각보다 오래 걸리고, 생각보다 어려우므로 이미 나온 알고리즘을 검토하라.
Testing Threaded Code
문제를 노출하는 테스트 케이스를 작성하라.
프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라.
테스트가 실패하면 원인을 추적하라.
다시 돌렸더니 통과하더라는 이유로 그냥 넘어가면 절대로 안 된다.
Treat Spurious Failures as Candidate Threading Issues
시스템 실패를 '일회성'이라 치부하지 마라
Get Your Nonthreaded Code Working First
스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅하지 마라
먼저 스레드 환경 밖에서 코드를 올바로 돌려라
Make Your Threaded Code Pluggable
다양한 설정에서 실행할 목적으로 다른 환경에 쉽게 끼워 넣을 수 있게 코드를 구현하라
Make Your Threaded Code Tunable
처음부터 다양한 설정으로 프로그램의 성능 측정 방법을 강구한다.
Run with More Threads Than Processors
시스템이 스레드를 스와핑할 때도 문제가 발생한다.
스와핑이 잦을수록 임계영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉬워진다.
Run on Differenr Platforms
운영체제마다 스레드를 처리하는 정책이 달라 결과가 달라질 수 있다.
처음부터 그리고 자주 모든 목표 플랫폼에서 코드를 돌려라
Instrument Your Code to Try and Force Failures
스레드 버그는 실패하는 경로가 실행될 확률이 극도로 저조해서 산발적이고 우발적이고 재현이 어렵다.
보조 코드를 추가해 코드가 실행되는 순서를 바꾼다.
Hand-Coded
코드에다 직접 wait(), sleep(), yeild(), priority() 함수를 추가한다.
Automated
보조 코드를 자동으로 추가하려면 AOF(Aspect-Oriented Framework), CGLB, ASM 등과 같은 도구를 사용한다.
Conclusion
📖 느낀점
스레드는 어렵다. 봐도 봐도 어렵다. 뭔 소리인지 모르겠다. 다른 책도 펼쳐봐야겠다.