2. Review of Chapter-13 동시성 I
• 다중 스레드(동시성) 코드는 올바로 구현하기가 어렵다
• 동시성의 오해
• 동시성은 항상 성능을 높여준다
• 동시성을 구현해도 설계는 변하지 않는다
• 동기화 라이브러리를 사용하면 동시성을 이해할 필요가 없다
• 동시성 방어 원칙
• SRP(단일 책임 원칙): 동시성은 복잡성 하나만으로도 따로 분리할 이유가 충분
• 공유 데이터의 최소화 및 캡슐화
• 복사본을 사용
• 스레드는 가능한한 독립적으로 구현
• 동시성 구현 코드 테스트
• 완벽한 증명하기는 현실적으로 불가능
• 많은 플랫폼에서 많은 구성으로 반복해서 테스트가 필요
12년 9월 10일 월요일
3. 클라이언트/서버 예제(1/4)
• Non-Thread Version
ServerSocket serverSocket = new ServerSocket(8009);
while (keepProcessing) {
try {
Socket socket = serverSocket.accept();
process(socket);
} catch (Exception e) {
handle(e);
}
}
• 만약 성능 이슈가 발생한다면?
• 응용 프로그램의 수행시간 종류
• 프로세서 - 수치 계산, 정규 표현식 처리, 가비지 컬렉션
• I/O - 소켓 사용, 데이터 베이스 연결, 가상 메모리 스와핑 기다리기
• 멀티 스레드로 성능 개선 가능
12년 9월 10일 월요일
4. 클라이언트/서버 예제(2/4)
• Thread Version
void process(final Socket socket) {
if (socket == null)
return;
Runnable clientHandler = new Runnable() {
public void run() {
try {
String message = MessageUtils.getMessage(socket);
MessageUtils.sendMessage(socket, "Processed: " + message);
closeIgnoringException(socket);
} catch (Exception e) {
e.printStackTrace();
}
}
};
Thread clientConnection = new Thread(clientHandler);
clientConnection.start();
}
• 이 코드의 문제점
• 하나 이상의 책임: 소켓 연결 관리, 클라이언트 처리, 스레드 정책, 서버 종료 정책
• 다양한 추상화 수준
12년 9월 10일 월요일
5. 클라이언트/서버 예제(3/4)
• 책임별로 클래스를 분리
• ClientConnection: 소켓 연결 관리
• ClientRequestProcessor: 클라이언트 처리
• ClientScheduler: 스레드 정책
• ConnectionManager: 서버 종료 정책
public void run() {
while (keepProcessing) {
try {
ClientConnection clientConnection = connectionManager.awaitClient();
ClientRequestProcessor requestProcessor = new ClientRequestProcessor(clientConnection);
clientScheduler.schedule(requestProcessor);
} catch (Exception e) {
e.printStackTrace();
}
}
connectionManager.shutdown();
}
12년 9월 10일 월요일
6. 클라이언트/서버 예제(4/4)
• 스레드 정책 코드 및 확장
public interface ClientScheduler {
public void schedule(ClientRequestProcessor requestProcessor);
}
public class ThreadPerRequestScheduler implements ClientScheduler {
@Override
public void schedule(final ClientRequestProcessor requestProcessor) {
Runnable runnable = new Runnable() {
public void run() {
requestProcessor.process();
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
// Java Executor framework을 사용한 Thread 정책 변경
public class ExecutorClientScheduler implements ClientScheduler {
Executor executor;
public ExecutorClientScheduler(int availableThreads) {
executor = Executors.newFixedThreadPool(availableThreads);
}
public void schedule(final ClientRequestProcessor requestProcessor) {
Runnable runnable = new Runnable() {
public void run() {
requestProcessor.process();
}
};
executor.execute(runnable);
}
}
12년 9월 10일 월요일
7. 가능한 실행 경로(1/2)
• 2개의 스레드로 이 코드를 실행한다면?
public class IdGenerator {
int lastIdUsed;
public int incrementValue() {
return ++lastIdUsed;
}
}
• lastIdUsed의 초기값이 93일 경우
• 스레드 1이 94를 얻고, 스레드 2가 95를 얻고, lastIdUsed가 95가 된다
• 스레드 1이 95를 얻고, 스레드 2가 94를 얻고, lastIdUsed가 95가 된다
• 스레드 1이 94를 얻고, 스레드 2가 94를 얻고, lastIdUsed가 94가 된다
• JVM의 구현 방식에 따라 이 결과도 가능
12년 9월 10일 월요일
8. 가능한 실행 경로(2/2)
• 경로의 수
• return ++lastIdUsed는 바이트 코드 8개로 구성됨
• 루프나 분기가 없이 명령 N개를 T개의 스레드로 실행했을때 가능한 경로의 수
• Ex) N = 8이고, T = 2이면 가능한 경로의 수: 12,870
• Synchronized를 적용했을 경우
• 가능한 경로의 수: 2(T)
public synchronized int incrementValue() {
return ++lastIdUsed;
}
12년 9월 10일 월요일
9. 라이브러리를 이해하라(1/3)
• Executor 프레임워크
• 스레드 풀 관리
• Runnable 인터페이스 지원
• Callable/Future 인터페이스 지원
• 스레드의 수행 결과를 받아오기 위해 사용
public String processRequest(String message) throws Exception {
Callable<String> makeExternalCall = new Callable<String>() {
public String call() throws Exception {
String result = "";
// make external request
return result;
}
};
Future<String> result = executorService.submit(makeExternalCall);
String partialResult = doSomeLocalProcessing();
return result.get() + partialResult;
}
12년 9월 10일 월요일
10. 라이브러리를 이해하라(2/3)
• 스레드를 중단하지 않는 방법
• Synchronized 및 Lock
• 비관적 잠금: 항상 락을 사용
public class ObjectWithValue {
private int value;
public synchronized void incrementValue() { ++value;}
public int getValue() { return value; }
}
• AtomicInteger 사용 - Concurrent package
• 낙관적 잠금: 현재 변수 값이 최종으로 알려진 값일 경우 갱신하고, 그렇지 않을 경우 성공할때까지 재 시도
• 프로세서의 CAS(Compare And Swap) 연산을 사용
public class ObjectWithValue {
private AtomicInteger value = new AtomicInteger();
public void incrementValue() { value.incrementAndGet(); }
public int getValue() { return value.get();
}
// CAS의 구현
int variableBeingSet;
void simulateNonBlockingSet(int newValue) {
int currentValue;
do {
currentValue = variableBeingSet;
} while (currentValue != compareAndSwap(currentValue, newValue));
}
12년 9월 10일 월요일
11. 라이브러리를 이해하라(3/3)
• 스레드에 안전하지 않은 클래스
• SimpleDateFormat, java.util 컨테이너 클래스...
• 해결 방안
• 스레드 안전 라이브러리 사용
• 직접 구현
• 클라이언트 기반 잠금 메커니즘
• 공유 데이터를 사용하는 모든 곳에서 lock을 사용
• 서버 기반 잠금 메커니즘
• 공유 데이터에 접근을 제어하는 별도의 클래스로 랩핑
12년 9월 10일 월요일
12. 메소드 사이에 존재하는 의존성을 조심하라(1/3)
• 의존성 예제
• 여러 메소드에서 하나의 변수를 공유해서 사용
public class IntegerIterator implements Iterator<Integer> {
private Integer nextValue = 0;
public synchronized boolean hasNext() {
return nextValue < 100000;
}
public synchronized Integer next() {
if (nextValue == 100000)
throw new IteratorPastEndException();
return nextValue++;
}
public synchronized Integer getNextValue() {
return nextValue;
}
}
• 만약 다중 스레드가 IntegerIterator 인스턴스 하나를 공유 한다면?
while(iterator.hasNext()) {
int nextValue = iterator.next();
// nextValue로 뭔가를 한
} 오류 발생: nextValue가 100000을 넘을 가능성이 존재
12년 9월 10일 월요일
13. 메소드 사이에 존재하는 의존성을 조심하라(2/3)
• 해결책
• 실패를 용인
• 클라이언트 기반 잠금: 모든 클라이언트에서 동기화 로직 구현
• DRY(Don’t Repeat Yourself) 원칙 위반: 중복으로 인한 오류 발생이 쉬움
while (true) {
int nextValue;
Client-based
synchronized (iterator) {
if (!iterator.hasNext())
break;
nextValue = iterator.next();
}
doSometingWith(nextValue);
Server-based
}
public class IntegerIteratorServerLocked {
private Integer nextValue = 0;
public synchronized Integer getNextOrNull() {
• 서버 기반 잠금
if (nextValue < 100000)
return nextValue++;
• 코드 중복이 줄어듬: 오류가 발생할 가능성이 줄어듬 else
return null;
}
• 성능이 좋아짐 }
while (true) {
• 스레드 정책이 하나임
int nextValue = iterator.getNextOrNull();;
if (next == null)
• 공유 변수가 줄어든다 break;
doSometingWith(nextValue);
}
12년 9월 10일 월요일
14. 메소드 사이에 존재하는 의존성을 조심하라(3/3)
• 서버 코드에 손대지 못하는 경우
• Adapter 패턴을 적용
public class ThreadSafeIntegerIterator {
private IntegerIterator iterator = new IntegerIterator();
public synchronized Integer getNextOrNull() {
if (iterator.hasNext())
return iterator.next();
return null;
}
}
12년 9월 10일 월요일
15. 작업 처리량 높이기
• EX) 네트워크에서 페이지를 읽어 분석하는 프로그램
• 가정
• 페이지를 읽어 오는 평균 I/O 시간: 1초
• 페이지를 분석하는 평균 처리시간: 0.5초
• 처리는 CPU 100% 사용, I/O는 CPU 0% 사용
• 단일 스레드 환경
• N 페이지를 처리하는 총 실행시간: 1.5초 * N
• 멀티 스레드 환경
• 3개의 스레드로 동시에 실행한다면?
• I/O 1초동안 다른 2개의 스레드는 페이지를 분석할 수 있어 단일 스레드에 비해 약 3배의 처리 속도를 가짐
12년 9월 10일 월요일
16. 데드락(1/2)
• 4가지 조건을 모두 만족하는 경우 데드락 발생
• 상호배제(Mutual exclusion)
use
Thread#1
Res1
Thread#2
not use
• 잠금&대기(Lock&Wait) use
Thread#1
Res1
Thread#2
wait
• 선점불가(No Preemption)
use
Thread#1
Res1
Thread#2
not preemptive
• 순환대기(Circular Wait)
lock Res1 request
Thread#2
Thread#1
request Res2 lock
12년 9월 10일 월요일
17. 데드락(2/2)
• 해결 방법
• 상호배제(Mutual exclusion)
• 스레드 수 이상으로 자원을 늘려서 해결 가능: 일반적으로 힘듬
• 잠금&대기(Lock&Wait)
• 필요한 모든 자원을 점유하지 못한다면 모든 자원을 반환하여 해결 가능
• 기아(Starvation), 라이브락(Livelock) 등의 문제가 발생할 수 있음
• 선점불가(No Preemption)
• 자원을 소유중인 스레드에게 해제를 요청하는 메커니즘을 사용하여 해결 가능
• 이러한 구현 및 관리가 쉽지 않음
• 순환대기(Circular Wait)
• 데드락을 방지하는 가장 흔한 전략중 하나
• 모든 자원을 똑같은 순서대로 할당하게 만들어 해결 가능
• 자원 할당 순서가 변경 불가능할 수 있음
12년 9월 10일 월요일
18. 다중 스레드 코드 테스트
• 다중 스레드를 테스트 하여 검증하기는 쉽지 않다
• 권장 방법
• 몬테 카를로 테스트
• 조율이 가능하게 유연한 테스트를 만들고, 임의의 값을 조율하며 반복하며 테스트
• 시스템을 배치할 모든 플랫폼에서 테스트
• 부하가 변하는 장비에서 테스트
12년 9월 10일 월요일