객체는 처리의 추상화다. 스레드는 일정의 추상화다.
동시성과 깔끔한 코드는 양립하기 어렵다.
다중 스레드 테스트해보기도 어렵다.
동시성은 결합(Coupling)을 없애는 전략이다.
동시성은 무엇(what)과 언제(when)을 분리하는 전략이다.
스레드가 하나인 프로그램은 무엇과 언제가 서로밀접하다.
무엇,언제를 분리하면 애플리케이션 구조와 효율이 극적으로 나아진다.응답 시간과 작업 처리량 개선에도 도움이 된다. 데이터를 대량으로 분석하는 시스템에서 병렬로 처리한다.
구조 개선의 좋은 예는 Servlet 모델일 것이다. 이론적으로, Servlet 개발자는 요청을 개별적으로 처리하는 데에만 신경을 쓰며 요청 큐를 직접 관리하는 부담을 덜 수 있다. 물론, Servlet이 제공하는 의존성의 해소는 완벽하지 않지만 Servlet이 제공하는 구조적인 이점은 그 자체로 가치가 있다.
1.동시성은 항상 성능을 높여준다. (무조건 성능을 높여주지는 않는다.)
대기 시간이 아주 길어 여러 스레드가 프로세서를 공유할 수 있거나, 여러 프로세서가 도이에 처리할 독립적인 계산이 충분한 경우에만 성능이 높아진다.
2.동시성을 구현해도 기존 설계는 변하지 않는다.
일반적으로 무엇과 언제를 분리하기 때문에 설계가 많이 변한다.
3.Web 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다.
어떻게 동작하는지 이해를 하고 있어야 하며, 동시 수정 문제, 데드락 문제를 피할 수 있는 방법을 알아야 한다.
그리고 동시성과 관련된 타당한 생각들을 살펴보자.
1.동시성은 다소 부하를 유발한다.
성능 측면에서 부하가 걸리며, 코드도 더 짜야한다.
2.동시성은 복잡하다.
간단한 문제라도 동시성은 복잡하다.
3.일반적으로 동시성 버그는 재현하기 어렵다.
그래서 진짜 결함으로 간주되지 않고 일회성 문제로 여겨 무시하기 쉽다.
4.동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.
public class ClassWithThreadingProblem {
private int lastIdUsed;
public ClassWithThreadingProblem(int lastIdUsed) {
this.lastIdUsed = lastIdUsed;
}
public int getNextId() {
return ++lastIdUsed;
}
}
public static void main(String args[]) {
final ClassWithThreadingProblem classWithThreadingProblem = new ClassWithThreadingProblem(42);
Runnable runnable = new Runnable() {
public void run() {
classWithThreadingProblem.getNextId();
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
}
위 코드가 만들 수 있는 결과는 총 3가지 이다.
t1이 43을, t2가 44를 가져간다. lastIdUsed는 44이다.(O) t1이 44을, t2가 43를 가져간다. lastIdUsed는 44이다.(O) t1이 43을, t2가 43를 가져간다. lastIdUsed는 43이다.(X)
위의 getNextId() 메서드는 8개의 자바 byte-code로 변환되며, 이를 두 스레드에서 실행하게 되면 총 12,870개의 코드 조합을 낼 수 있다.
그 중 얼마 안 되는 몇몇 조합이 위의 3가지 결과 중 마지막 결과를 낳게 된다.
지금부터 동시성 코드가 일으키는 문제로부터 시스템을 방어하는 원칙과 기술을 소개한다.
SRP는 주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 한다는 원칙이다.
동시성은 복잡성 하나만으로도 따로 분리할 이유가 충분하다. 즉, 동시성 관련 코드는 다른 코드와 분리해야 한다는 뜻이다.
동시성을 구현할 때는 다음 몇 가지를 고려한다.
1.동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다. 2.동시성 코드에는 독자적인 난관이 있다. 다른 코드에서 겪는 난관과 다르며 훨씬 어렵다. 3.잘못 구현한 동시성 코드는 별의별 방식으로 실패한다. 주변에 있는 다른 코드가 발목을 잡지 않더라도 동시성 하나만으로 충분히 어렵다.
- 권장사항 : 동시성 코드는 다른 코드롸 분리하라!
공유 객체를 두 스레드에서 수정하는 중 간섭이 발생할 수 있으며 이는 예기치 못한 결과를 야기할 수 있다.
이러한 임계영역(critical section)을 보호하는 한 가지 방법은 synchronized 키워드를 사용하는 것이다.
임계영역 의 수는 가능한한 적게 만들어야 하며 이를 어길 경우 아래와 같은 문제가 발생하기 쉽게 된다.
1.보호할 임계영역을 빼먹는다. 그래서 공유 자료를 수정하는 모든 코드를 망가뜨린다.
2.모든 임계영역을 올바르게 보호했는지 확인하느라 똑같은 노력과 수고를 반복한다.
3.그렇지 않아도 찾아내기 어려운 버그가 더욱 찾기 어려워진다.
- 권장사항 : 자료를 캡슐화 하라. 공유 자료를 최대한 줄여라.
공유 자원 문제를 해결하는 좋은 방법중 하나는 애초에 공유 자원을 사용하지 않는 것이다.
읽기 전용으로 사용될 경우 자원의 복사본을 사용하게 하는 방법이 있다.
경우에 따라서는 복사본을 여러 스레드에 전달, 작업을 수행하고 결과를 단일 스레드에서 수집해 사용하는 것도 가능하다.
객체의 복사에 드는 비용을 걱정할 수도 있다.
혹은, 이 문제가 "진짜 문제가 되는지" 조사해 보는 방법도 있다.
하지만 객체의 복사본을 사용함으로써 동기화를 피할 수 있다면, 객체 생성 및 GC에 드는 비용은 공유 자원 동기화에 필요한 비용 보다 일반적으로 적은 비용으로 문제를 해결하게 해준다.(객체 복사 cost < 공유 자원 동기화 cost)
스레드 코드를 공유 자원을 사용하지 않는 독립된 세계로 만든다면 동기화 문제는 없어지게 된다.
HttpServlet을 생각해 보라.
HttpServlet을 상속받는 클래스는 doGet, doPost와 같은 메서드에서 필요한 파라미터를 받아 처리한다.
이는 각 Servlet이 각자의 세계에 있는 것처럼 작동하게 도와주며, 지역 변수를 사용하는 한 동기화 문제는 발생하지 않게 된다.
물론 대부분의 Servlet들은 데이터베이스 연결과 같은 공유 자원이 필요하긴 하다.
- 권장사항 : 데이터를 독립적인 스레드-더 나아가 각각의 프로세서-에서 사용될 수 있게 구분하라.
자바 5는 동시성 측면에서 이전 버전보다 많이 나아졌다.
자바 5로 스레드 관련 코드 작성시 아래의 사항들을 숙지하자.
1.자바에서 제공하는 thread-safe 컬랙션을 사용하라. 2.연관이 없는 태스크들을 수행시 executor 프레임워크를 사용하라. 3.가능하면 스레드가 차단되지 않는 방법(nonblocking)을 사용하라. 4.몇몇 라이브러리 클래스들은 thread-safe하지 않다.
####스레드 환경에 안전한 컬렉션
java.util.concurrent 패키지는 멀티 스레드 환경에서 사용할 수 있는 컬랙션들을 제공한다.
ConcurrentHashMap의 경우에는 일반 HashMap보다 대부분의 상황에서 더 좋은 퍼포먼스를 제공한다.
만약 배포 환경이 자바 5 버전 이상이라면 이 패키지를 활용하자.
아래와 같은 고급 동시성 디자인 구현을 위한 컴포넌트들도 숙지하자.
1.ReentrantLock : 한 메서드에서 잠그고 다른 메서드에서 해제될 수 있는 lock 이다. 2.Semaphore : 전통적인 세마포어(갯수를 셀 수 있는 lock)의 구현체 이다. 3.CountDownLatch : 기다리는 모든 스레드들을 해제하기 전 특정 횟수의 이벤트가 발생하는 것을 기다리게 할 수 있는 lock이다. 모든 스레드가 거의 동시에 시작될 수 있게 도와줄 수 있다.
- 권장사항: 언어가 제공하는 클래스를 검토하라. 지바에서는 java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.locks를 살펴보라.
실행 모델에 대해 이야기하기 위해 필요한 기본적인 용어를 먼저 알아 보자.
1.한정된 자원(Bound Resource) : 환경에서 사용되는 고정된 크기의 자원이다. 예시로 데이터베이스 연결, 고정된 크기의 읽기/쓰기 버퍼가 있다.
2.상호 배제(Mutual Exclusion) : 한 시점에 공유 자원에 접근할 수 있는 스레드는 단 하나이다.
3.기아(Starvation) : 한 스레드 혹은 스레드의 그룹이 긴 시간 혹은 영원히 작업을 수행할 수 없게 된다. 작업의 우선권을 가지는 수행 시간이 짧은 스레드가 끝없이 실행된다면 수행 시간이 긴 스레드는 굶게 된다.
4.데드탁(Deadlock) : 두 개 이상의 스레드들이 서로의 작업이 끝나기를 기다린다. 각 스레드는 서로가 필요로 하는 자원을 점유하고 있으며 필요한 자원을 얻지 못하는 이상 그 누구도 작업을 끝내지 못하게 된다.
5.라이브락(Livelock) : 스레드들이 서로 작업을 수행하려는 중 다른 스레드가 작업중인 것을 인지하고 서로 양보한다. 이러한 공명 때문에 스레드들은 작업을 계속 수행하려 하지만 장시간 혹은 영원히 작업을 수행하지 못하게 된다.
1.생산자-소비자(Producer-Consumer)
한 개 이상의 생산자가 생산한 작업물을 버퍼 혹은 큐에 넣는다.
한 개 이상의 소비자가 버퍼 혹은 큐에서 작업물을 습득, 작업을 마친다.
생산자와 소비자 사이에 있는 큐는 bound resource이다. 따라서 생산자는 큐에 남는 공간이 생길 때까지, 소비자는 큐에 작업물이 하나라도 생길 때까지 기다려야 한다.
큐를 통한 생산자와 소비자간의 조율에는 둘 사이의 시그널링이 필요하다.
생산자는 큐에 작업물을 넣고 소비자에게 "큐가 비어있지 않다"는 신호를 보내고 소비자는 큐에서 작업물을 꺼낸 후 "큐가 가득차 있지 않다"는 신호를 보낸다. 그 전까지 둘은 신호를 기다린다.
2.읽기-쓰기(Readers-Writers)
일반적으로 독자를 위한 정보로 사용되며, 가끔 저자에 의해 업데이트되는 공유 자원의 경우 처리량이 문제가 된다.
처리량을 강조해 독자가 상대적인 우선권을 가지게 되면 저자는 기아 상태에 빠지며 공유 자원은 정체된 정보로 가득차게 된다.
반대로 저자가 우선권을 가지면 처리량이 줄어들게 된다.
저자-독자 문제는 이 둘 사이의 균형을 맞추며 concurrent 업데이트를 방지하는 것을 주안점으로 둔다.
3.식사하는 철학자들(Dining Philosophers)
원탁을 둘러싼 여러 명의 철학자들이 있다.
각 철학자의 왼쪽에 포크가 놓여 있으며 테이블의 중앙에 큰 스파게티 한 그릇이 놓여 있다.
그들은 배가 고파지기 전까지 각자 생각을 하며 시간을 보낸다.
배가 고파지면 그들은 자신의 양쪽에 놓여 있는 포크 2개를 잡고 스파게티를 먹는다.
철학자는 포크 2개가 있어야만 스파게티를 먹을 수 있다.
그렇지 않다면 옆 사람이 포크를 다 사용하기 전까지 기다려야 한다.
스파게티를 먹은 철학자는 다시 배가 고파질 때까지 포크를 놓고 있게 된다.
위 상황에서 철학자를 스레드로, 포크를 공유 자원으로 바꾸게 되면 이는 자원을 놓고 경쟁하는 프로세스와 비슷한 상황이 된다.
잘 설계되지 않은 시스템은 deadlock, livelock, 처리량 문제, 효율성 저하 문제에 맞닥뜨리기 쉽다.
당신이 맞닥뜨릴 대부분의 동시성 관련 문제들은 이 세 가지 문제의 변형일 가능성이 높다.
이 알고리즘들을 공부하고 스스로 해법을 작성함으로써 이와 같은 문제들을 직면하더라도 의연하게 대처할 수 있도록 하자.
- 권장사항 : 위에서 설명한 기본 알고리즘과 각 해법을 이해하라.
동기화된 메서드 간의 의존성은 동시 코드에서 사소한 버그를 일으킬 수 있다. 성
자바는 synchronized라는 "메서드 하나를 보호하는 노테이션"을 제공한다.
하지만 한 클래스에 두 개 이상의 synchronized 메서드가 존재하면 문제를 일으킬 수도 있다.
- 권장사항 : 공유 객체 하나에는 메서드 하나만 사용하라.
만약 위 추천을 따를 수 없는 상황이라면 아래의 세 방법을 고려한다.
1.클라이언트 기반 잠금(Client-Based Locking)
클라이언트가 첫 메서드를 부르기 이전부터 마지막 메서드를 부른 다음까지 서버를 잠근다. (역주: 공유 객체를 사용하는 코드에서 공유 객체를 잠그는 것이다.)
=> Bad: 서버를 사용하는 모든 클라이언트 코드에서 lock이 필요하게 되며 이는 유지보수 및 디버깅에 필요한 비용을 상승시킨다.
2.서버 기반 잠금(Server-Based Locking)
서버 내에서 서버(자신)을 잠그고 모든 동작을 수행한 후 잠금을 푸는 메서드를 제공한다.
클라이언트에게는 새로운 메서드를 제공한다. (역주: 공유 객체에 새로운 메서드를 작성하고 잠금이 필요한 동작 전체를 수행하게 하는 것이다.)
=> Good: Critical section에 접근하는 코드를 최소화해 위 4-2에 부합한다.
3.중계된 서버(Adapted Server)
잠금을 수행하는 중계자를 작성한다.
이는 기본적으로 서버 기반 잠금이지만 기존의 서버를 변경할 수 없는 상황에 사용할 수 있는 방법이다.(역주: 서드 파티 라이브러리를 사용한다고 생각하면 쉬울 것이다.)
Synchronized로 수행되는 잠금은 딜레이와 오버헤드를 만들기 때문에 "비싼 수행"으로 간주되며 가능한 한 작게 만들어야 한다.
반면 critical section은 꼭 보호되어야 한다.
- 권장사항 : 동기화된 영역은 최대한 작게 만들어라.
"항상 살아 있어야 하는 코드"의 작성은 "잠시 동작하고 조용히 끝나는" 코드의 작성과는 다르다.
조용히 끝나는 코드는 작성하기 어렵다. 이는 보편적으로 "오지 않을 신호"를 기다리는 쓰레드의 데드락을 포함한다.
데드락에 걸린 자식 스레드의 수행이 끝나길 기다리는 부모 스레드의 경우를 생각해 보라. 자식은 데드락에 걸려 멈춰 있고 부모는 이를 끝없이 기다리게 된다.
이와 같은 코드를 작성할 경우 정상적인 종료가 이루어질 때까지 많은 시간이 소요될 것을 상정해야 한다.
- 권장사항 : 종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라. 생각보다 오래 걸린다. 생각보다 어려우므로 이미 나온 알고리즘을 검토하라.
테스트는 정확성을 보장하지 않으며 "코드가 제대로 작성되었는가"를 증명할 수 없다.
다만 잘 작성된 테스트는 위험을 최소화할 수 있다. 이는 멀티 스레드 상황에서는 훨씬 더 복잡해 진다.
- 권장사항 : 문제를 발생시킬 만한 테스트를 작성하고 여러 프로그램 설정과 시스템 설정, 부하 하에서 자주 수행하라.
테스트가 한번이라도 실패한다면 원인을 분석하라. 한번 더 테스트를 실행해 성공했다고 해서 이전의 실패를 무시하지 마라.
이는 아래와 같이 분류할 수 있다.
1.말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라 2.다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자 3.다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드를 구현하라 4.다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라 5.프로세서 수보다 많은 스레드를 돌려보라 6.다른 플랫폼에서 돌려보라 7.코드에 보조 코드(instrument)를 넣어 돌려라. 강제로 실패를 일으키게 해보라
멀티 스레드 코드는 일반적으로 발생할 리 없어 보이는 문제를 발생시킨다.
(저자를 포함한)대부분의 개발자는 이러한 문제를 직관적으로 파악하지 못한다.
또한 이는 매우 드물게 발생해 개발자들을 좌절하게 만든다.
그래서 개발자들은 이러한 문제들을 우주선(宇宙線), 하드웨어 버그, 혹은 이러한 류의 one-off로 치부한다. 제일 좋은 방향은 one-off는 없다고 판단하는 것이다.
이러한 one-off들이 무시될 수록 더 많은 코드들이 이미 문제가 있는 시스템에 추가되게 될 뿐이다.
- 권장사항 : 시스템 오작동을 일회성(one-off)로 판단해 무시하지 말라.
당연한 말이지만 거듭 강조할 만큼 중요한 이야기이다. 스
레드 밖에서 잘 동작하는 코드를 먼저 작성하라. 이는 스레드에서 사용될 POJO를 뜻한다.
POJO는 스레드와 연관이 없어 스레드 밖에서도 테스트할 수 있다. 시스템은 가능한 한 POJO로 작성하는 것이 좋다.
- 권장사항 : 스레드 관련 버그와 그렇지 않은 버그를 동시에 잡으려 하지 마라. 작성한 코드가 스레드 밖에서 잘 작동하는지 먼저 체크하라.
동시성 지원 코드를 아래와 같이 여러 설정으로 실행될 수 있게 만들어라.
1.단일 스레드, 여러 스레드 환경에서 동작하는지 실행 중 스레드 수를 바꿔본다. 2.스레드 코드를 실제 환경이나 테스트 환경에서 돌려본다. 3.테스트 코드를 빨리, 천천히, 다양한 속도로 돌려본다. 4.반복 테스트가 가능하도록 테스트 케이스를 작성한다.
- 권장사항 : 다양한 설정에서 실행할 목적으로 다른 환경에 쉽게 끼워 넣을 수 있게 코드를 구현하라.
스레드 관련 코드의 적절한 균형을 맞추는 작업은 보통 시행착오를 필요로 한다.
여러 환경에서 시스템의 퍼포먼스를 테스트할 수 있는 방법을 개발 초기에 강구하라.
실행할 스레드 갯수를 쉽게 변경할 수 있게 작성하라. 이를 시스템이 동작하는 도중에 변경할 수 있게 하는 것을 고려해 보라.
처리량과 시스템 활용도를 기준으로 스스로를 조정할 수 있게 하는 것을 고려해 보라.
시스템이 작업을 전환할 때에도 문제는 발생한다.
작업 전환을 빈번히 발생하게 하기 위해 프로세서 수보다 많은 스레드를 실행해 보라.
작업 전환이 잦을수록 빠뜨린 critical section이나 dead lock을 찾을 확률이 높아지게 된다.
우리(저자)는 2007년 중순 concurrent 프로그래밍 강좌를 개발했다.
강좌의 개발은 OSX에서 진행되었으며 시연은 VM상의 Windows XP에서 진행되었다.
하지만 실패를 시연하기 위해 작성된 테스트는 OSX에서는 자주 발생했지만 Windows XP에서는 OSX에서만큼 자주 발생하지 않았다.
우리는 이로 인해 서로 다른 운영체제는 상이한 스레딩 정책을 가지며 코드의 실행에 영향을 미친다는 것을 다시 한번 깨닫게 되었다.
멀티 스레드 코드는 실행 환경에 따라 다르게 동작한다. 따라서 당신은 모든 잠재적 배포 환경에 대해 테스트를 수행해야 한다.
- 권장사항 : 처음부터 그리고 자주 모든 목표 플랫폼에서 코드를 돌려라.
스레드 관련 문제는 수많은 실행 경로중 얼마 안되는 확률로 발생하기 때문에 드물게 발생하며 재현하기 어렵다.
이 실행 경로를 조작해 스레드 문제가 발생할 확률을 높이는 보조 코드에는 두 가지 방법이 있다.
1.직접 구현하기 2.자동화
이는 Object.wait(), Object.sleep(), Object.yield(), Object.priority()등의 메서드를 사용해 실행 경로를 변경함으로써 코드의 문제를 발견하는 방법이다.
public synchronized String nextUrlOrNull() {
if(hasNext()) {
String url = urlGenerator.next();
Thread.yield();
// inserted for testing.
updateHasNext();
return url;
}
return null;
}
yield() 메서드를 호출함으로써 코드의 실행 경로를 변경할 수 있다.
만약 위 코드에서 문제가 발생한다면 이는 yield()를 추가해 생긴 문제가 아니라 이미 존재하던 문제를 명백히 만든것 뿐이다.
하지만 이 방법에는 몇 가지 문제가 있다.
1.테스트할 부분을 직접 찾아야 한다. 2.어디에 어느 메서드를 호출해야 할지 알기 어렵다. 3.이와 같은 코드를 제품에 포함해 배포하는 것은 불필요하게 퍼포먼스를 저하시킬 뿐이다. 4.Shotgun approach 8이기 때문에 반드시 문제가 발생한다는 보장을 얻을 수 없다. 5.우리는 실제 제품에 포함되지 않으며 여러 조합으로 실행해 에러를 찾기 쉽게 만들 방법이 필요하다.
이를 위해서는 시스템을 최대한 POJO 단위로 나눠 instrument code를 삽입할 부분을 찾기 쉽게 하고 여러 정책에 따라 sleep, yield등을 삽입할 수 있게 해야 한다.
위와 다르게 Aspect-oriented Framework, CGLib, ASM등을 통해 프로그램적으로 코드를 조작할 수도 있다. 아래의 예를 보자.
public class ThreadJigglePoint {
public static void jiggle() { }
}
public synchronized String nextUrlOrNull() {
if(hasNext()) {
ThreadJiglePoint.jiggle();
String url = urlGenerator.next();
ThreadJiglePoint.jiggle();
updateHasNext();
ThreadJiglePoint.jiggle();
return url;
}
return null;
}
위와 같이 구현한 후 간단한 Aspect 9를 이용해 '아무 것도 안하기', 'sleep', 'yield'등을 무작위로 선택하게 할 수 있다.
혹은 ThreadJigglePoint가 두 가지 구현을 가지게 할 수도 있다.
첫 번째 구현은 배포용 코드를 위한 '아무 것도 안하기'를 수행하며 두 번째 구현은 'sleep, yield, 아무 것도 안하기' 중의 하나를 무작위로 선택하는 것이다.
다소 간단하긴 하지만 좀 더 정교한 툴을 사용하는 대신 이 정도로 구현하는 것도 적절한 선택일 것이다.
혹은 이와 비슷한 작업을 수행해 주는 IBM에서 개발한 ConTest라는 툴도 있다.
이는 수행시마다 다른 순서로 스레드를 실행하게 만들어 줌으로써 문제를 발견할 확률을 극적으로 높여준다.
동시 코드는 제대로 작성하기 어렵다. 이해하기 쉬운 코드는 여러 스레드와 공유 자원이 엮이게 되면 끔찍한 결말을 낳게 된다. 성
당신이 concurrent code를 작성하게 된다면 엄격한 기준으로 clean 하게 작성하라. 그렇지 않으면 찾기 어렵고 빈번하지 않은 오류를 만나게 될 것이다.
최우선적으로 SRP를 숙지하라. 시스템을 최대한 POJO단위로 잘라 스레드 관련 코드와 非 스레드 관련 코드를 나누어라.
스레드 관련 코드를 테스트할 때에는 그 이외의 것들은 제외하고 스레드 관련 문제만 테스트하라. 이는 스레드 관련 문제가 최대한 작은 부분에 집중되게 한다.
한 공유 자원에 대한 멀티 스레드 수행, 공유되는 자원 풀 등 concurrency 문제를 일으킬 수 있는 부분에 대해 인지하라.
깔끔하게 종료되게 하는 문제나 반복문 탈출과 같은 문제는 특히 성가실 수 있다.
라이브러리를 이해하고 기본적인 알고리즘을 이해하라. 라이브러리가 제공하는 기능이 어떻게 문제를 해결하는지 이해하라.
잠가야 할 필요가 있는 부분을 찾는 방법을 배우고 잠가라. 쓸데 없는 구간을 잠그지 마라.
잠긴 구간에서 또 다른 잠긴 구간을 부르는 것을 기피하라. 이는 "무엇이 공유되고 안되고"에 대한 깊은 이해를 요구한다.
공유 객체의 갯수와 공유 영역을 최소한으로 줄여라. 클라이언트가 공유 객체의 상태(잠금 등)를 관리하는 대신 공유 객체의 디자인을 변경하라.
문제는 돌연 발생할 것이다. 그렇지 않은 문제들은 보통 "한번만 발생하는" 문제로 치부된다.
이러한 one-off들은 보통 시스템에 부하가 걸린 경우, 혹은 무작위로 발생한다. 그러므로 스레드 관련 코드는 여러 설정, 환경에서 반복적이고 지속적으로 수행해 보라.
당신의 코드를 시간을 들여 instrument하게 되면 문제점을 찾을 확률은 높아질 것이다. 직접 코드를 작성할 수도 있고 자동화 툴을 사용할 수도 있다. 출시하기 전까지 최대한 오래 테스트 해야할 것이다.
Clean한 접근 방식을 사용한다면, 제대로 된 코드를 만들어낼 가능성은 급격히 올라갈 것이다.