18
Appendix-A 동시성 II chois79 12910월요일

Clean code appendix 1

Embed Size (px)

Citation preview

Appendix-A 동시성 II

chois79

12년 9월 10일 월요일

Review of Chapter-13 동시성 I

• 다중 스레드(동시성) 코드는 올바로 구현하기가 어렵다

• 동시성의 오해

• 동시성은 항상 성능을 높여준다

• 동시성을 구현해도 설계는 변하지 않는다

• 동기화 라이브러리를 사용하면 동시성을 이해할 필요가 없다

• 동시성 방어 원칙

• SRP(단일 책임 원칙): 동시성은 복잡성 하나만으로도 따로 분리할 이유가 충분

• 공유 데이터의 최소화 및 캡슐화

• 복사본을 사용

• 스레드는 가능한한 독립적으로 구현

• 동시성 구현 코드 테스트

• 완벽한 증명하기는 현실적으로 불가능

• 많은 플랫폼에서 많은 구성으로 반복해서 테스트가 필요

12년 9월 10일 월요일

클라이언트/서버 예제(1/4)

• Non-Thread Version

• 만약 성능 이슈가 발생한다면?

• 응용 프로그램의 수행시간 종류

• 프로세서 - 수치 계산, 정규 표현식 처리, 가비지 컬렉션

• I/O - 소켓 사용, 데이터 베이스 연결, 가상 메모리 스와핑 기다리기

• 멀티 스레드로 성능 개선 가능

ServerSocket serverSocket = new ServerSocket(8009);while (keepProcessing) { try { Socket socket = serverSocket.accept(); process(socket); } catch (Exception e) {

handle(e); }}

12년 9월 10일 월요일

클라이언트/서버 예제(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일 월요일

클라이언트/서버 예제(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일 월요일

클라이언트/서버 예제(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일 월요일

가능한 실행 경로(1/2)

• 2개의 스레드로 이 코드를 실행한다면?

• lastIdUsed의 초기값이 93일 경우

• 스레드 1이 94를 얻고, 스레드 2가 95를 얻고, lastIdUsed가 95가 된다

• 스레드 1이 95를 얻고, 스레드 2가 94를 얻고, lastIdUsed가 95가 된다

• 스레드 1이 94를 얻고, 스레드 2가 94를 얻고, lastIdUsed가 94가 된다

• JVM의 구현 방식에 따라 이 결과도 가능

public class IdGenerator { int lastIdUsed;

public int incrementValue() { return ++lastIdUsed; }}

12년 9월 10일 월요일

가능한 실행 경로(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일 월요일

라이브러리를 이해하라(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일 월요일

라이브러리를 이해하라(2/3)

• 스레드를 중단하지 않는 방법

• Synchronized 및 Lock

• 비관적 잠금: 항상 락을 사용

• AtomicInteger 사용 - Concurrent package

• 낙관적 잠금: 현재 변수 값이 최종으로 알려진 값일 경우 갱신하고, 그렇지 않을 경우 성공할때까지 재 시도

• 프로세서의 CAS(Compare And Swap) 연산을 사용

public class ObjectWithValue { private int value; public synchronized void incrementValue() { ++value;} public int getValue() { return value; }}

// CAS의 구현int variableBeingSet;void simulateNonBlockingSet(int newValue) { int currentValue; do { currentValue = variableBeingSet; } while (currentValue != compareAndSwap(currentValue, newValue));}

public class ObjectWithValue { private AtomicInteger value = new AtomicInteger(); public void incrementValue() { value.incrementAndGet(); } public int getValue() { return value.get(); }

12년 9월 10일 월요일

라이브러리를 이해하라(3/3)

• 스레드에 안전하지 않은 클래스

• SimpleDateFormat, java.util 컨테이너 클래스...

• 해결 방안

• 스레드 안전 라이브러리 사용

• 직접 구현

• 클라이언트 기반 잠금 메커니즘

• 공유 데이터를 사용하는 모든 곳에서 lock을 사용

• 서버 기반 잠금 메커니즘

• 공유 데이터에 접근을 제어하는 별도의 클래스로 랩핑

12년 9월 10일 월요일

메소드 사이에 존재하는 의존성을 조심하라(1/3)

• 의존성 예제

• 여러 메소드에서 하나의 변수를 공유해서 사용

• 만약 다중 스레드가 IntegerIterator 인스턴스 하나를 공유 한다면?

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; }}

while(iterator.hasNext()) { int nextValue = iterator.next(); // nextValue로 뭔가를 한} 오류 발생: nextValue가 100000을 넘을 가능성이 존재

12년 9월 10일 월요일

메소드 사이에 존재하는 의존성을 조심하라(2/3)

• 해결책

• 실패를 용인

• 클라이언트 기반 잠금: 모든 클라이언트에서 동기화 로직 구현

• DRY(Don’t Repeat Yourself) 원칙 위반: 중복으로 인한 오류 발생이 쉬움

• 서버 기반 잠금

• 코드 중복이 줄어듬: 오류가 발생할 가능성이 줄어듬

• 성능이 좋아짐

• 스레드 정책이 하나임

• 공유 변수가 줄어든다

while (true) { int nextValue; synchronized (iterator) { if (!iterator.hasNext()) break; nextValue = iterator.next(); } doSometingWith(nextValue);} 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);}

Client-based

Server-based

12년 9월 10일 월요일

메소드 사이에 존재하는 의존성을 조심하라(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일 월요일

작업 처리량 높이기

• 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일 월요일

데드락(1/2)

• 4가지 조건을 모두 만족하는 경우 데드락 발생

• 상호배제(Mutual exclusion)

• 잠금&대기(Lock&Wait)

• 선점불가(No Preemption)

• 순환대기(Circular Wait)

Thread#1 Thread#2

Res2

Res1lock

lock

request

request

Thread#1

Thread#2

Res1

use

not preemptive

Thread#1

Thread#2

Res1

use

wait

Thread#1

Thread#2

Res1

use

not use

12년 9월 10일 월요일

데드락(2/2)

• 해결 방법

• 상호배제(Mutual exclusion)

• 스레드 수 이상으로 자원을 늘려서 해결 가능: 일반적으로 힘듬

• 잠금&대기(Lock&Wait)

• 필요한 모든 자원을 점유하지 못한다면 모든 자원을 반환하여 해결 가능

• 기아(Starvation), 라이브락(Livelock) 등의 문제가 발생할 수 있음

• 선점불가(No Preemption)

• 자원을 소유중인 스레드에게 해제를 요청하는 메커니즘을 사용하여 해결 가능

• 이러한 구현 및 관리가 쉽지 않음

• 순환대기(Circular Wait)

• 데드락을 방지하는 가장 흔한 전략중 하나

• 모든 자원을 똑같은 순서대로 할당하게 만들어 해결 가능

• 자원 할당 순서가 변경 불가능할 수 있음

12년 9월 10일 월요일

다중 스레드 코드 테스트

• 다중 스레드를 테스트 하여 검증하기는 쉽지 않다

• 권장 방법

• 몬테 카를로 테스트

• 조율이 가능하게 유연한 테스트를 만들고, 임의의 값을 조율하며 반복하며 테스트

• 시스템을 배치할 모든 플랫폼에서 테스트

• 부하가 변하는 장비에서 테스트

12년 9월 10일 월요일