728x90
반응형
13장. 동시성
동시성은 왜 필요한가?
- 결합(coupling)을 없애는 전략이다. 싱글스레드는 what, when이 밀접하다. 이것을 분리한다.
- 구조적 개선뿐만이 아닌 응답 시간(response time), 출력량(throughput)을 위해서도 필요하다.
- 메일 정보 수집기
- 한명씩만 처리하는 시스템
동시성의 미신과 오해
- 동시성은 성능을 높여준다
동시성은 때로 성능을 높여준다.
즉 항상 높여주는것은 아니다. 여러 스레드가 프로세서를 공유할 수 있거나, 여러 프로세서에서 동시에 처리할 독립적 계산이 많은 경우에만 성능이 높아진다. (일반적인 상황은 아님)
- 동시성을 구현해도 설계는 변하지 않는다(X)
- 싱글 스레드와, 다중 스레드는 설계가 다르다. what,when을 분리하면 시스템 구조가 크게 달라진다.
- 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다(X)
- 그렇지 않다. 어떻게 동시 수정, 데드락을 피할 수 있는지, 실제로 컨테이너가 어떻게 동작하는지 알아야한다
Race Condition으로 인한 문제
public class X {
private int lastIDUsed;
public int getNextId() {
return ++lastIdUsed;
}
}
- 2개의 쓰레드가 번갈아가며 getNextId를 호출한다면 순서대로 값이 올라갈까?
- 그렇지 않다. 왜 그럴까?
- JAVA로 생각하면..
- 메서드를 실행하는 잠재적인 경로의 수가 굉장히 많다
- JIT 컴파일러가 바이트 코드를 처리하는 방식, 자바 메모리 모델이 원자로 간주하는 최소 단위를 알아야 한다.
- C로 생각하면..
- +1을 하는 과정에서도 LOAD, ADD ,STORE 최소 3가지를 거쳐야 하는데, 이 과정에서 LOAD, ADD , STORE을 하는 사이에 다른 스레드의 명령이 들어갈 수 있음. 따라서 공유 자원을 사용할땐 조심해야함.
동시성 방어원칙
- 공유자원을 최대한 줄이자
- 너무 많으면 보호해야할 critical section을 놓치게 된다
- SRP
- 주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나라는 원칙.
- 동시성 관련 코드는 다른 코드와 분리해야 한다. (복잡성 하나만으로 분리할 이유가 충분)
- 자료 사본 (copy) 사용하기
- 이 말은 즉 공유자원을 쓰지 않겠다는 말
- lock을 걸고 하는 시간보다, copy와 gc에 드는 부하가 비슷할 수 있다. 구현의 용이함에는 사본이 더 편하긴 함.
- 각 스레드가, 클라이언트 요청 하나를 처리하게 독립적으로 구현한다 (현실성 떨어져보임)
- 모든 정보를 비공유 출처에서 가져오고 로컬변수에 저장. 다른스레드와 동기화 필요 없음.
- 라이브러리 이해하기
- thread-safe컬렉션 사용하기.
- non-blocking 방법 사용하기
- JAVA 5의 ConcurrentHashMap
기본 용어
- 한정된 자원(Bound Resource)
- 상호 배제(Mutual Exclusion)
- 기아(Starvation)
- 데드락(Deadlock)
- 라이브락(Livelock)
실행 모델 이해하기
- Producer-Consumer(생산자 소비자 문제)
- 1개 이상의 producer thread는 정보를 생성해서 큐나, 버퍼에 저장한다
- 1개 이상의 consumer thread는 대기열에서 정보를 가져와서 사용한다.
- 큐와 버퍼는 사이즈가 유한함.
- Producer는 큐나 버퍼가 꽉차면 기다려야함.
- Consumer는 큐나 버퍼가 비어있으면 정보를 줄 때까지 기다려야함.
- Producer는 정보를 넣고 Consumer에게 정보를 넣었다고 시그널, Consumer는 Producer에게 큐에 빈공간이 있다고 시그널을 보냄.
- 문제 발생..
- 큐가 꽉 찬 상황에서, Consumer가 하나를 읽고 다른 스레드에게 자리가 났다고 보냈지만, 그 스레드가 갑자기 죽어버렸으면, 시그널을 못받고 Producer는 신호가 올때까지 영원히 대기할 수 있음..
- Readers-Writers
- 읽기 쓰레드를 위한 정보를 shared resource에서 가져온다고 가정.
- 쓰기 쓰레드는 공유자원을 가끔 갱신함
- throughput을 강조하면 starvation이 생기거나, 오래된 정보가 쌓인다.
- 갱신을 허용하면 처리율에 영향을 미친다. Writer가 버퍼 갱신하는 동안 Reader가 버퍼를 읽지 않고, 그 반대로 읽는동안엔 갱신을 하면 안되는데 이 균형을 잡기가 어렵다.
- 일반적으로 Writer가 버퍼를 오랫동안 점유해서 Reader가 기다리느라 throughput이 떨어진다.
- *
양쪽 균형을 잡는 방법이 필요
하다.
- Dining Philosophers
- 철학자를 스레드로, 포크를 자원으로 생각해보자
- 한 쓰레드가 양쪽의 자원을 모두 가져야 하는데, 계속 한곳씩 사용하고 있다면 그 쓰레드는 영원히 기다리게 될 것이다.
동시성 프로그램 잘 만들기
- 임계영역 수를 최대한 줄이자. 동기화 하는 영역을 줄이는것이 좋다. (락은 스레드를 지연시키고 부하를 증가시킴)
- 순차 코드부터 잘 돌게 만들자. 한번에 동시성 프로그램을 만들기는 어렵다.
- cpu의 코어 개수보다 많은 스레드 돌려보기
- thread swap시에도 문제가 발생하기에, 코어 개수보다 많은 스레드를 돌리면 데드락을 일으키거나, 임계영역 처리를 놓친 부분을 찾기 쉽다.
- 다른 플랫폼에서 돌려보자.
- 강제로 실패를 일으키게 해보자
- wait(), sleep() , yield()같은것들을 넣어보자
결론
- 다중 스레드 코드는 짜기가 어렵다.
- SRP를 준수하자. 다중 스레드 관련 코드는 반드시 분리하자.
- 동시성 오류를 일으키는 잠정적 원인을 철저히 이해해야 한다.
- lock은 필요한 부분만. 임계영역은 최소로..
728x90
반응형
'WEB > Clean Code' 카테고리의 다른 글
[Clean Code] 12장. 창발성 (0) | 2022.02.24 |
---|---|
[Clean Code] 11장. 시스템 (0) | 2022.02.24 |
[Clean Code] 10장. 클래스 (0) | 2022.02.23 |
[Clean Code] 9장. 단위테스트 (0) | 2022.02.23 |
[Clean Code] 8장. 경계 (0) | 2022.02.22 |