Software Engineering Note

부록 A. 동시성2 본문

스터디/Clean Code

부록 A. 동시성2

devmoons 2017. 2. 13. 23:29

클라이언트/서버 예제에서는 단일 스레드로 동작하던 서버를 다중 스레드로 변경하는 것과 코드를 깨끗하게 변경하는 내용을 다룬다.


다중 스레드를 적용하기전에 어플리케이션이 어디서 시간을 보내는지 알아야 한다.


I/O - 소켓 사용, 데이터베이스 연결, 가상 메모리 스와핑 기다리기 등에 시간을 보낸다.


프로세서 - 수치계산, 정규표현식 처리, 가비지 컬렉션 등에 시간을 보낸다.


프로세서 연산에 시간을 보내는 프로그램은 스레드를 늘인다고 빨라지지않는다. CPU 사이클은 한계가 있기 때문이다.


다중 스레드 프로그램을 깨끗하게 유지하려면 잘 통제된 몇 곳으로 스레드 관리를 모아야 한다. 

아니, 스레드를 관리하는 코드는 스레드만 관리해야 한다. 

비동시성 문제까지 뒤섞지 않더라도 동시성 문제는 그 자체만으로도 추적하기 어려운 탓이다.


동시성 그 자체가 복잡한 문제이므로 다중 스레드 프로그램에서는 단일 책임 원칙이 특히 중요하다.



애플리케이션에서 스레드는 생성하나 스레드 풀을 사용하지 않는다면 혹은 직접 생성한 스레드 풀을 사용한다면


Executor 클래스를 고려하기 바란다. 코드가 깔끔해지고, 이해하기 쉬워지고, 크기가 작아진다.



스레드를 차단하지 않는 방법 (non blocking)


AtomicBoolean, AtomicInteger 등의 여러 클래스를 사용한다. 


이 방법이 synchronized보다 성능이 좋은데, CAS (Compare and Swap) 연산을 이용하기때문이다. 


synchronized 키워드는 언제나 락을 건다. 다른 스레드가 같은 값을 갱신하지 않더라도 무조건 락부터 건다.

자바 버전이 올라갈 때마다 내장 락의 성능이 좋아지기는 했지만 그래도 락을 거는 대가는 여전히 비싸다.


다중 스레드 환경에서 안전하지 않은 클래스


- SimpleDateFormat

- 데이터베이스 연결

- java.util 컨테이너 클래스

- 서블릿


몇몇 collection 클래스는 스레드에 안전한 메서드를 제공 하지만, 그런 메서드 여럿을 호출하는 작업은 스레드에 안전하지않다.


예를들어,


if (!hashTable.containsKey(someKey)) {

    hashTable.put(someKey, new SomeValue());

}


각 메서드는 스레드에 안전하지만, 두 메서드 사이에 다른 스레드가 끼어들어 값을 추가할지도 모른다.


해결방안 세가지


1. 클라이언트 기반 잠금. 사용하는 곳에서 처리


2. HashTable을 객체로 감싼 후 다른 API를 사용


3. 스레드에 안전한 집합 클래스를 사용

- java.util.concurrent 패키지가 제공하는 집합 클래스는 putIfAbsent() 등과 같이 스레드에 안전한 메서드를 제공한다.



메서드 사이에 존재하는 의존성을 조심하라


각 메서드는 synchronized 키워드로 락을 걸었지만, 메서드 사이의 의존성때문에 문제가 발생하는 경우. iterator 예제가 그렇다.


해결방안 세가지


1. 실패를 용인한다. 예외를 받아 처리하는 식으로.. (다소 조잡한 방법)


2. 클라이언트 기반 잠금

- 각 클라이언트가 synchronized 키워드를 이용해서 락을 걸고, 메서드 호출

- DRY를 위반한다. 클라이언트마다 이짓을 해야하므로


3. 서버 기반 잠금

- iterator 기반클래스에서 락을 건다.

- 클라이언트 기반 잠금에 비해 코드 중복이 없다.


일반적으로 서버 기반 잠금이 더 바람직하다.


- 코드중복이 줄어든다.

- 성능이 좋아진다. 

- 오류가 발생할 가능성이 줄어든다.

- 스레드 정책이 하나다.

- 공유 변수 범위가 줄어든다. (서버에 숨겨짐)



데드락


다음 네 가지 조건을 모두 만족하면 데드락이 발생한다.


1. 상호 배제

- 여러 스레드가 한 자원을 공유하나 그 자원은 "여러 스레드가 동시에 사용하지 못하며, 개수가 제한적"

- ex) 데이터베이스 연결, 쓰기용 파일 열기, 레코드 락, 세마포어 등


2. 잠금과 대기

- 스레드가 자원을 점유하면 필요한 나머지 자원까지 모두 점유해 작업을 마칠때까지 이미 점유한 자원을 내놓지않는다.


3. 선점 불가

- 한 스레드가 다른 스레드로부터 자원을 빼앗지 못한다.


4. 순환 대기

- 두 개의 스레드가 서로가 필요로 하는 자원을 점유하고 있으면서, 다른 스레드가 가진 자원을 필요로 할때


네 조건 중 하나라도 깨버리면 데드락은 발생하지 않는다.


1. 상호 배제 조건 깨기

- 동시에 사용해도 괜찮은 자원을 사용한다. 

- 스레드 수 이상으로 자원수를 늘린다.

- 자원을 점유하기 전에 필요한 자원이 모두 있는지 확인한다.


2. 잠금 & 대기조건 깨기

각 자원을 점유하기 전에 확인한다.

만약 어느 하나라도 점유하지 못한다면 지금까지 점유한 자원을 몽땅 내려놓고 처음부터 다시 시작한다.


문제점

* 기아 - 한 스레드가 계속해서 필요한 자원을 점유하지 못한다.

* 라이브락 - 여러 스레드가 한꺼번에 잠금 단계로 진입하는 바람에 계속해서 자원을 점유했다 내놨다를 반복한다.


3. 선점 불가 조건 깨기

필요한 자원이 잠겼다면 자원을 소유한 스레드에게 풀어달라고 요청.


4. 순환 대기 조건 깨기

데드락을 방지하는 가장 흔한 전략이다.

모든 스레드가 일정순서에 동의하고 그 순서로만 자원을 할당


문제점

* 자원을 할당하는 순서와 자원을 사용하는 순서가 다를지도 모른다. (= 자원을 필요이상 오랫동안 점유한다)

* 때로는 순서에 따라 자원을 할당하기 어렵다.



'스터디 > Clean Code' 카테고리의 다른 글

17장. 냄새와 휴리스틱  (0) 2016.12.12
14장. 점진적인 개선  (0) 2015.12.14
13장. 동시성  (0) 2015.10.20
12장. 창발성  (0) 2015.09.06
11장. 시스템  (0) 2015.09.06