매번 ubuntu를 AWS에서 사용할 때, date 찍히는 날짜가 한국 기준 +9라 정말 기분이 안 좋았다.
이에 다음과 같은 명령을 발견한다.
dpkg-reconfigure tzdata
여기서 Asia 그리고 Seoul을 선택하였다.
그런데, 네트워크 끊겨서(putty 접속 중) 다시 들어가려 하니 위 명령어를 쳤을 때 다른 프로세스에서 사용중이라 락걸렸다고 변경을 허용해주지 않는다.
이런, w 눌러서 나 이외의 사용자 죽이고 dpkg 프로세스 찾아 강제로 죽이고 다시 했다.
tzselect는 .profile 같은 거 수정해야 하는듯? 안 썼다.
내멋대로
상관
2016년 6월 15일 수요일
2016년 2월 27일 토요일
[일기] Java 동시성(Concurrentcy) Atomic and ConcurrencyMap Part 1
출처 : http://winterbe.com/posts/2015/05/22/java8-concurrency-tutorial-atomic-concurrent-map-examples/
마지막이다. 이제 자바의 Conncurecy API에서 중요한 두 부분을 다룬다. Atomic Variables과 Concurrent Map 이 그것이다. 두 개념 모두 자바 8에서 람다식과 함수형 프로그래밍의 도입과 함께 상당히 개선되었다. 새로운 특징들을 쉬운 예제와 함께 살펴보게 된다.
Part 1. Threads and Executors (완)
Part 2. Synchronzation and Locks (완)
Part 3. Atomic Variables and ConcurrentMap
여기서도 Part 2에서와 마찬가지로 Sleep과 stop의 메서드를 이용할 것이다.
public static void stop(ExecutorService executor) {
try {
executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
System.err.println("termination interrupted");
}
finally {
if (!executor.isTerminated()) {
System.err.println("killing non-finished tasks");
}
executor.shutdownNow();
}
}
public static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
AtomicInteger
패키지 중 java.concurrent.atomic 을 살펴보면 원자적 연산을 수행할 수 있는 유용한 클래스들을 확인할 수 있다. 어떤 연신이 Part 2에서 다뤄봤던 Lock이나 Synchronized 키워드 없이도 여러 스레드들에 의해 병렬적으로 수행되어도 결과의 안전성을 보장받을 수 있다면 그 연산은 원자적이라고 한다.
내부적으로 원자적 클래스들은 CAS를( compare-and-swap) 십분 활용한다. 간단히 설명하자면, 값을 변경할 때 자신이 읽었던 변수의 값을 기억하고 있다가 변경 직전에 변수의 메모리 내의 값을 확인하여 이전에 기억해놓은 값과 같은 경우에만 처리를 진행하고 그렇지 않은 경우는 무산시키는 방식이다. 이러한 명령은 현대 CPU에 의해 직접적으로 지원된다(하드웨어적으로). 그래야 원자적 연산이 가능하다. 운영체제 시간에 배우겠지만 하드웨어의 지원이 없이는 완벽한 원자적 연산을 지원하는 것이 불가능하다. 따라서 요새는 CPU 차원에서(하드웨어 차원에서) 이러한 연산을 지원하므로 특정 편수에 여러 스레드들이 동시에 접근하여 작업하는 경우가 있는 경우 이러한 원자적 연산을 이용하는 클래스를 이용하는 것이 권장된다.
이제 한 원자적 클래스의 예를 하나 살펴보도록 하자. 그 첫번째 타자는 AtomicInteger이다.
AtomicInteger atomicInt = new AtomicInteger(0);ExecutorService executor = Executors.newFixedThreadPool(2);IntStream.range(0, 1000).forEach(i -> executor.submit(atomicInt::incrementAndGet));stop(executor);System.out.println(atomicInt.get()); // => 1000
단지 Integer 대신에 AtomicInteger를 사용했을 뿐이다. Integer는 스레드-안전 하지 않으며 따라서 변수에 접근할 때 동기화를 신경써주지 않는다면 값의 안정적인 결과를 보장받을 수 없다. 위에서 사용한 incrementAndGet 메서드는 원자적 연산을 수행하며 따라서 우리는 아주 손쉽게 멀티 스레드로부터 안전한 연산을 수행할 수 있다.
AtomicInteger는 다양한 원자적 연산을 지원한다. updateAndGet메서드는 람다 표현식을 인자로 받을 수 있는데 이 람다식을 통해 어떤 연산을 수행할지를 입맛대로 정의할 수 있다.
AtomicInteger atomicInt = new AtomicInteger(0);ExecutorService executor = Executors.newFixedThreadPool(2);IntStream.range(0, 1000).forEach(i -> {Runnable task = () ->atomicInt.updateAndGet(n -> n + 2);executor.submit(task);});stop(executor);System.out.println(atomicInt.get()); // => 2000
다음 살펴볼 메서드는 accumulateAndGet() 이다. 이 메서드도 람다식을 인자로 받지만 들어가는 람다식은 IntBinaryoperator 타입이다. 다음 예제에서 이 메서드를 이용해 0부터 1000의 값을 모두 더한 값을 구할 수 있다.
AtomicInteger atomicInt = new AtomicInteger(0);ExecutorService executor = Executors.newFixedThreadPool(2);IntStream.range(0, 1000).forEach(i -> {Runnable task = () ->atomicInt.accumulateAndGet(i, (n, m) -> n + m);executor.submit(task);});stop(executor);System.out.println(atomicInt.get()); // => 499500
이 외에도 유용한 클래스로는 AtomicBoolean, AotmicLong 그리고 AtomicReferece가 있다.
LongAdder
이 클래스는 AtomicLong에 상응하는 클래스다. 연속적으로 숫자를 더할 때 사용할 수 있다.
ExecutorService executor = Executors.newFixedThreadPool(2);IntStream.range(0, 1000).forEach(i -> executor.submit(adder::increment));stop(executor);System.out.println(adder.sumThenReset()); // => 1000
LongAdder는 add()와 increment() 메서드를 다른 원자적 숫자 클래스와 마찬가지로 제공한다. 그리고 역시 스레드-안전하다. 그러나 하나의 결과만을 도출하는 대신에 이 클래스는 내부적으로 다른 스레드들의 경쟁을 감소시키기 위해 변수들의 집합을 유지하고 있다. 그 실제 결과는 sum()과 sumThenReset() 을 통해 얻을 수 있다.
이 클래스는 대개 여러 스레드에 의해 읽기보다 업데이트가 많이 일어나는 원자적 숫자를 다룰때 유용하고 바람직하게 여겨진다. 특히 통계 데이터를 뽑아낼 때와 같은 경우가 있다. 특정 웹 서버가 처리한 요청의 숫자를 셈한다거나 할 수 있다. 결점이 있다면 굉장하게 메모리를 소모한다는 점인데 전술하였듯이 내부적으로 수많은 값들의 집합을 유지하고 있기 때문이다.
LongAccumulator
이놈은 LongAdder의 일반화된 버전이다. 단순히 덧셈을 수행하는 대신하기 보다 LongBinaryOperator의 람다식을 이용한다.
LongBinaryOperator op = (x, y) -> 2 * x + y;LongAccumulator accumulator = new LongAccumulator(op, 1L);ExecutorService executor = Executors.newFixedThreadPool(2);IntStream.range(0, 10).forEach(i -> executor.submit(() -> accumulator.accumulate(i)));stop(executor);System.out.println(accumulator.getThenReset()); // => 2539
LongAccumulator를 하나 만들고 2 * x + y의 람다식을 인자로 주었다. 그리고 초기값을 1로 지정했다. 매순간 accumulate(i) 가 호출될 때마다 현재의 결과와 i의 값은 람다식의 인자로 넘어간다. 이 클래스도 LongAdder 와 마찬가지로 내부적으로 스레드간의 경합을 줄이기 위해 변수의 집합을 유지한다.
그런데 결과과 왜 2539일까. 이건 이것대로 꽤나 머리가 아프다. i가 y의 값으로 리턴된 현재 값이 x로 들어간다면 우리가 차근차근 계산하면 다음과 같이 나와야 한다.
x y result1 0 > 22 1 >55 2 > 1212 3 > 2727 4 > 5858 5 > 121121 6 > 248248 7 > 503503 8 > 10141014 9 > 2037
아니 그런데 왜 2539인가.. 이유는 다음과 같다. 우리는 싱글 스레드가 아니라 멀티 스레드로 돌렸기 때문이다. 잘 생각해보면 스레드가 2개가 돈다. 따라서 다음과 같이 들어간다.
x : y >1, 0 result : 2x : y >1, 1 result : 3x : y >2, 2 result : 6x : y >6, 3 result : 15x : y >15, 4 result : 34x : y >34, 5 result : 73x : y >73, 6 result : 152x : y >152, 7 result : 311x : y >311, 8 result : 630x : y >630, 9 result : 1269x : y >1269, 1 result : 25392539
또는
x : y >1, 1 result : 3x : y >1, 0 result : 2x : y >3, 2 result : 8x : y >8, 3 result : 19x : y >19, 4 result : 42x : y >42, 5 result : 89x : y >89, 6 result : 184x : y >184, 7 result : 375x : y >375, 8 result : 758x : y >758, 9 result : 1525x : y >1525, 0 result : 30503050
중요하게 넘어갈 것이 있다. java doc에 따르면 accumulator 의 동작은 보장되지 않는다. 따라서 연산의 순서가 문제되지 않는 현상에서 응용가능하다고 적혀 있다. 즉 (2 * x + y)는 순서에 의존적이므로 문제(교환/결합법칙 불가능)가 된다. (x + 2* y)라면 순서가 문제가 되지 않을 것이다.
0 에서 10은 언제 어떻게 순서를 바꿔서 더하든 결과는 같다. 1 + 2 + 5 + 3 + 4 = 1 + 5 + 2 + 4 + 3. 그러나 곱하는 건 다르다.
시간이 늦어서 나머진 나중에 적기로 한다.
[일기] Java 동시성(Concurrency) Synchronization and Locks(락)
출처 : http://winterbe.com/posts/2015/04/30/java8-concurrency-tutorial-synchronized-locks-examples/
Java 8의 무지 쉬운 예제를 통하여 멀티 프로그래밍에 대한 개념을 소개하는 두번째 튜토리얼의 글을 보게 된 것을 환영한다. 또다시 약 15분을 투자하여 어떻게 자바에서 락, 세마포어와 같은 동기화 키워드로 대표되는 개념들을 통해 공용 변수에 동기적으로 접근할 수 있는지 알게 될 것이다.
Part 1. Threads and Executors(완)
Part 2. Synchronization and Locks
Part 3. Atomic Variables and ConcurrentMap
여기서는 Part 1에서 알아본 내용을 이용하여 아래와 같은 도우미 메서드(Helper method)를 정의하여 사용하도록 하겠다.
public class ConcurrentUtils {public static void stop(ExecutorService executor) {try {executor.shutdown();executor.awaitTermination(60, TimeUnit.SECONDS);}catch (InterruptedException e) {System.err.println("termination interrupted");}finally {if (!executor.isTerminated()) {System.err.println("killing non-finished tasks");}executor.shutdownNow();}}public static void sleep(int seconds) {try {TimeUnit.SECONDS.sleep(seconds);} catch (InterruptedException e) {throw new IllegalStateException(e);}}}
Sleep과 Stop을 기억하라.
Synchronzied
이전 Part 1에서 executor service를 통해 어떻게 코드를 병렬적으로 실행시키는지 알아보았다. 멀티 스레드로 프로그램을 작성하게 될 때는 여러 스레드에 의해 공용으로 접근되는 변수에 접근할 때 각별한 주의를 기울여야 한다.
아래 코드에서 count 변수를 하나 설정하였고 메서드 increment()를 정의했다.
int count = 0;void increment() {count = count + 1;}
굉장히 간단한 코드이다. increment() 메서드는 변수 count의 값을 1 증가시킨다. 자 이 메서드를 아래와 같이 호출해보자.
ExecutorService executor = Executors.newFixedThreadPool(2);IntStream.range(0, 10000).forEach(i -> executor.submit(this::increment));stop(executor);System.out.println(count); // 9965
설명도 할 것 없이, 0부터 10000까지 즉, executor에 increment 함수를 넘겨서 10000회 호출시킨다. 결과를 보자면 count의 값은 10000이 되어야 마땅하다. 그러나 실제 결과는 매번 다르다. 그리고 절대 10000이 되는 경우를 보기는 쉽지 않다. 그 이유는 동기화 없이 무분별하게 다수의 스레드에 의해 변수에 접근하게 되면 흔히들 Race condition 이라는 현상을 만나기 때문이다.
변수의 값을 증가시키는 과정은 다음과 같이 나뉜다 첫째로 현재 변수의 값을 읽어내고, 두번째로 1 증가된 값을 알아낸 다음, 세번째로 그 값을 해당 변수의 값으로 대입시킨다. 이것이 한번에 순차대로 모두 일어나면 문제가 없겠지만 두개 이상의 스레드에 의해 진행되면 문제가 생길 수 있다. 초기에 count 변수의 값은 0이다. 스레드는 cpu 시간을 사용하여 움직인다. 스레드 1이 cpu에 의해 구동되어 첫번째, 두번째 단계까지 마무리 짓고 차례가 스레드 2로 넘어갔다고 하자. 스레드 1은 0이란 값을 읽었고 다음 값을 계산하여 1이란 결론을 얻은 상태이다. 스레드 2는 운이 좋게도 첫번째, 두번째 그리고 세번째 단계를 모두 진행했다. 그리고 다시 스레드 1의 차례가 되었다. count 의 현재 값은 스레드 2에 의해 1이 되었을 것이다. 그러나 스레드 1은 세번째 단계를 수행하여 count 변수에 자신이 계산한 값 1을 대입하게 되고 결과적으로 count는 2가 아닌 1의 값을 갖는다.
대강 그렇고, 아마 운영체제 시간에 배울 것이다.
다행스럽게도 이를 위해 자바는 일찍부터 synchronized라는 키워드를 통해 스레드 동기화를 지원하고 있었다. 우리는 synchronized 키워드를 이용해서 위 race condition 을 정상적으로 실행되도록 고칠 수 있다.
synchronized void incrementSync() {count = count + 1;}
즉, 동기화 메서드로 incrementSync 메서드를 배타적으로 접근하게 만든 것이다. 위 코드에서 executor에 incrementSync를 넘겨주면 예상한 결과값인 10000이 count의 값으로 할당되었다는 사실을 알 수 있다.
ExecutorService executor = Executors.newFixedThreadPool(2);IntStream.range(0, 10000).forEach(i -> executor.submit(this::incrementSync));stop(executor);System.out.println(count); // 10000
synchronized 키워드는 블록 구문으로도 사용 가능하다.
void incrementSync() {synchronized (this) {count = count + 1;}}
내부적으로 자바는 소위 monitor라 불리는 개념을 동기화를 위해 사용한다. 역시 운영체제에서 알 수 있을 내용이며 monitor lock or intrinsic lock을 보면 나오고 지금 당장 몰라도 된다고 치자. 이 모니터는 한 객체에 바인딩 되는데 메서드에 동기화를 걸게 되면 각 메서드는 해당 상응하는 객체에 대해 동일한 모니터를 공유한다.
모든 암시적인 모니터는 재진입 가능한(reentrant) 특징을 가진다. 이게 무슨 뜻이냐 하면 한 스레드는 같은 락을 여러번 얻어낼 수 있다는 뜻이다. 한 동기화 메서드에 진입하여 특정 객체에 대해 락을 얻은 스레드는 같은 객체를 락으로 사용하는 다른 동기화 메서드를 호출할 수 있다.
Locks
synchronzied 키워드를 이용한 암시적인 락킹을 이용하는 대신에 Lock 인터페이스로 정의된 명시적인 락을 Concurrency API를 통해 사용할 수 있다. Locks은 매우 다양한 메서드를 제공하는데 무척이나 훌륭한 특징을 갖는 락 제어를 가능케 한다. 따라서 암시적인 모니터보다 좀더 강렬하다.
다수의 락 구현체들이 자바 표준 JDK에 있으며 다음 섹션에서 소개한다.
ReentrantLock
ReentrantLock 클래스다. 상호 배타적 락인데 기본적으로 synchronized 키워드를 이용한 암시적인 모니터와 같이 동작한다. 차이가 있다면 더 많은 기능을 제공한달까. 이름에서 나타나듯 이 락은 재진입 가능한 특징을 같는다.
어떻게 이용하는지 살펴보자.
ReentrantLock lock = new ReentrantLock();int count = 0;void increment() {lock.lock();try {count++;} finally {lock.unlock();}}
lock() 메서드를 통해 락을 획득한다. 그리고 unlock() 메서드를 통해 락을 해제한다. try / finally 블록으로 동기화 처리될 부분의 코드를 감싸는 것이 중요하다. 그래야만 예외상황에서도 락을 해제하지 않는 일을 방지할 수 있기 때문이다. 위 메서드는 이제 스레드-안전하다. 동기화 메서드와 동일하다. 만일 다른 스레드가 이미 락을 갖고 있다면 이후 lock()이 호출되면 현재 스레드는 락이 풀릴 때까지 정지된다. 오직 단 하나의 스레드만이 락을 특정 시간동안 획득할 수 있다.
락은 훌륭한 동기화 제어를 위해 다음과 같이 다양한 메서드를 제공한다.
ExecutorService executor = Executors.newFixedThreadPool(2);ReentrantLock lock = new ReentrantLock();executor.submit(() -> {lock.lock();try {sleep(1);} finally {lock.unlock();}});executor.submit(() -> {System.out.println("Locked: " + lock.isLocked());System.out.println("Held by me: " + lock.isHeldByCurrentThread());boolean locked = lock.tryLock();System.out.println("Lock acquired: " + locked);});stop(executor);
첫번째 작업은 락을 1초동안 가지고 있을 것이며 두번째 작업은 락에 대한 상태를 출력한다.
Locked: trueHeld by me: falseLock acquired: false
tryLock() 메서드는 lock()을 대신할 수 있는 메서드이며 스레드가 정지되는 상황에 들어가지 않으면서 락을 획득하려는 시도를 하게 해준다. 결과로 리턴되는 진위값은 공용 변수에 접근하기 전에 해당 스레드가 락을 획득했는지 그렇지 못했는지를 판단하는 데 사용된다.
ReadWriteLock
인터페이스 ReadWriteLock은 읽기와 쓰기 접근에 대한 대해 락을 운영할 수 있는 새로운 타입의 락이다. 이 아이디어는 변하지 않는 변수를 여럿이 동시에 읽기만 할 때는 대개 문제가 발생하지 않고 쓸때만 문제가 발생한다는 점에 착안해 나왔다. 그러므로 읽기-락은 동시에 여러 스레드에 의해 특정 스레드가 쓰기 락을 요청하지 않는 한 계속 획득될 수 있다. 이런 방식으로 쓰기보다 읽기가 더 빈번히 일어나는 상황에서는 성능을 높일 수 있다.
ExecutorService executor = Executors.newFixedThreadPool(2);Map<String, String> map = new HashMap<>();ReadWriteLock lock = new ReentrantReadWriteLock();executor.submit(() -> {lock.writeLock().lock();try {sleep(1);map.put("foo", "bar");} finally {lock.writeLock().unlock();}});
위 예제는 처음에 쓰기-락을 요구한다. 제일 처음에는 map에 데이터를 넣어야 하기 때문이다. 그리고 1초간 잔다. 이 약 1초간의 작업이 끝나기 이전에 두개의 다른 작업은 executor에 등록되는데 이 등록되는 작업들은 map에서 값을 읽고 출력한 뒤 1초간 잔다.
Runnable readTask = () -> {lock.readLock().lock();try {System.out.println(map.get("foo"));sleep(1);} finally {lock.readLock().unlock();}};executor.submit(readTask);executor.submit(readTask);stop(executor);
위 코드들을 실행해보면 알겠지만 두 읽기를 수해하는 작업은 쓰기가 완료되는 전체 시간동안 반드시 지연된다. 쓰기 작업이 끝나서 쓰기-락이 해제된 시점에 두 읽기 작업은 비로소 구동이 시작되고 둘 중 어느 하나라도 막히는 바 없이 동시에 작업을 수행하게 된다. 읽기 작업 간에는 서로가 서로를 기다려야 할 필요가 없기 때문이다.
StampedLock
자바 8은 새로운 종류의 락을 하나 또 가져왔는데, 그 이름은 StampedLock이다. 이 또한 마찬가지로 읽기, 쓰기 락을 바로 위에서 살펴본 바와 같이 지원한다. 그러나 ReadWriteLock과는 대조적으로 락을 구하는 메서드는 Long 타입의 stamp를 리턴한다(Timestamp의 stamp...와 유사). 이 stamp 값을 이용해서 락을 해제하거나 락이 아직 타당한지를 확인해 볼 수 있다. 게다가 stamped locks은 optimistic locking이라 불리는 또다른 락 모드를 지원한다.
마지막으로 봤었던 예제를 ReadWriteLock 대신에 StampedLock을 이용하여 수정해보자.
ExecutorService executor = Executors.newFixedThreadPool(2);Map<String, String> map = new HashMap<>();StampedLock lock = new StampedLock();executor.submit(() -> {long stamp = lock.writeLock();try {sleep(1);map.put("foo", "bar");} finally {lock.unlockWrite(stamp);}});Runnable readTask = () -> {long stamp = lock.readLock();try {System.out.println(map.get("foo"));sleep(1);} finally {lock.unlockRead(stamp);}};executor.submit(readTask);executor.submit(readTask);stop(executor);
readLock() 혹은 writeLock()을 호출하여 읽기와 쓰기 락을 얻을 수 있다. 그리고 두 메서드는 stamp 값을 리턴하고 나중에 finally 블록에서 락을 해제할 때 사용된다. 여기서 알고 넘어가야 점이 있는데 stamped lock은 재진입 가능한 특징이 없다는 것이다. 각각의 락을 요구하는 메서드의 호출은 새로운 stamp를 리턴하고 락을 얻을 수 없으면 블럭된다. 이전에 락을 얻었던 동일한 스레드가 락을 요구해도 블럭된다. 그러니 데드락에 빠지지 않게 무척이나 주의해야 한다.
이전의 ReadWriteLock 예제와 같이 두 읽기 작업은 쓰기 작업이 완료되어 쓰기-락이 풀릴 때까지 반드시 기다려야 한다. 그리고 읽기 작업이 수행되면 동시에 코드에 접근하여 값을 콘솔에 출력할 것이다. 쓰기-락이 새로 요구되지 않는 한 읽기 작업은 서로를 블럭시키지 않기 때문이다.
다음은 optimistic locking이다.
ExecutorService executor = Executors.newFixedThreadPool(2);StampedLock lock = new StampedLock();executor.submit(() -> {long stamp = lock.tryOptimisticRead();try {System.out.println("Optimistic Lock Valid: " + lock.validate(stamp));sleep(1);System.out.println("Optimistic Lock Valid: " + lock.validate(stamp));sleep(2);System.out.println("Optimistic Lock Valid: " + lock.validate(stamp));} finally {lock.unlock(stamp);}});executor.submit(() -> {long stamp = lock.writeLock();try {System.out.println("Write Lock acquired");sleep(2);} finally {lock.unlock(stamp);System.out.println("Write done");}});stop(executor);
한 최적 읽기 락은 tryOptimisticRead() 메서드를 이용하여 획득가능하며 이 메서드는 항상 현재 스레드를 블럭시키는 것 없이 무조건 stamp 값을 리턴한다. 즉 현재 락을 획득할 수 있건 없건 상관않고 리턴한다. 락을 얻었는지 못 얻었는지는 stamp의 값이 0이냐 아니냐를 가지고 판단해야 한다. 0이면 못 얻은거다. 또한 해당 stamp 값이 현재 유효한지 아닌지를 lock.validate(stamp) 방식으로 알아낼 수 있다.
Optimistic Lock Valid: trueWrite Lock acquiredOptimistic Lock Valid: falseWrite doneOptimistic Lock Valid: false
결과는 위와 같은데 잘 생각하면 이해가 된다. 초기에 읽기 락이 획득되었다. 따라서 첫 호출에선 true 가 리턴된다. 그러나 1초를 쉬게 되고 이때 두번째 작업에서 쓰기 락을 획득한다. 이 경우, 읽기능 수행이 불가능해지며 따라서 다음 호출문에서는 false가 리턴된다. 쓰기는 2초 후에 끝나게 되고 동시에 쓰기-락도 해제된다. 그러면 이제 읽기-락은 다시 활성화 되어 true가 리턴된다. 그러나 잘 보면 마지막 출력은 true가 아니라 false가 리턴되었다. 이 말은 다음과 같다. 최적-락에서 읽기-락의 경우 한번 비활성화되면 끝까지 안살아난다는 거다. 따라서 매번 공용 변수를 접근하고 나서는 읽기가 타당함을 보증하기 위해 락을 활성화시켜줘야 한다.
가끔은 읽기-락을 해제하지 않고 쓰기 락을 얻는 것이 유용할 때까 있다. 그럴 때 StampedLock은 tryConvertToWirteLock() 을 딱 그 목적을 위해 제공한다.
ExecutorService executor = Executors.newFixedThreadPool(2);StampedLock lock = new StampedLock();executor.submit(() -> {long stamp = lock.readLock();try {if (count == 0) {stamp = lock.tryConvertToWriteLock(stamp);if (stamp == 0L) {System.out.println("Could not convert to write lock");stamp = lock.writeLock();}count = 23;}System.out.println(count);} finally {lock.unlock(stamp);}});stop(executor);
위 작업을 보면 첫번째로 읽기-락을 획득하여 count 필트의 값을 출력한다. 그러나 현재 값이 0이라면 그 값을 23으로 바꾸고 싶었기에(그러고 싶었다고 하자), 읽기-락을 쓰기 락으로 곧바로 전환해야 했다. 끊고 새로 쓰기-락을 요청하려면 그 사이에 다른 스레드가 들어와 무슨 일을 저지를지 모를 일이기 때문이다. tryConvertToWriteLock() 메서드를 호출하면 블럭되는 일이란 없다. 단지 당장 쓰기-락을 획득할 수 없는 경우 0 값의 stamp를 리턴하게 된다. 이 경우 어쩔 수 없이 writeLock()을 호출한다. 그리고 사용가능한 쓰기-락이 생길 때까지 블럭된다.
Semaphores
세마포어는 암세포어다. Concurrency API는 락 이외에도 counting semaphores를 지원한다. 락은 단순히 특정 변수나 자원에 대한 배타적인 접근권을 부여하는 반면에, 세마포어는 허가권들의 집합을 운영하는 형태라 생각할 수 있다. 이 개념은 다음과 같은 시나리오에서 굉장히 유용하다. 한 자원에 대해 제한된 수만의 스레드만 접근하도록 설정해야 하는 경우다.
ExecutorService executor = Executors.newFixedThreadPool(10);Semaphore semaphore = new Semaphore(5);Runnable longRunningTask = () -> {boolean permit = false;try {permit = semaphore.tryAcquire(1, TimeUnit.SECONDS);if (permit) {System.out.println("Semaphore acquired");sleep(5);} else {System.out.println("Could not acquire semaphore");}} catch (InterruptedException e) {throw new IllegalStateException(e);} finally {if (permit) {semaphore.release();}}}IntStream.range(0, 10).forEach(i -> executor.submit(longRunningTask));stop(executor);
위에서 만든 executor는 잠재적으로 10개의 스레드를 돌릴 수 있다. 그러나 우리는 5 크기의 세마포어만을 사용했다. 따라서 위 코드에는 최대 5개의 스레드만이 동시에 접근할 수 있다. 여기서도 역시 try/finally 블럭을 이용하여 어떤 예외에서도 세마포어를 해제해 줄 수 있도록 설정했다.
결과는 이렇다.
Semaphore acquiredSemaphore acquiredSemaphore acquiredSemaphore acquiredSemaphore acquiredCould not acquire semaphoreCould not acquire semaphoreCould not acquire semaphoreCould not acquire semaphoreCould not acquire semaphore
tryAcquire()가 실행될 때마다 초기 5개에 대해서는 세마포어를 할당해 접근을 허가하지만 그 이후부터는 얻을때까지 최대 1초까지 기다리라고 인자에 넣은 것처럼 1초 기다리고 리턴하게 된다. 더 이상의 세마포어는 없기 때문이다.
[일기] Java 동시성(Concurrency) Threads and Executors
출처 : http://winterbe.com/posts/2015/04/07/java8-concurrency-tutorial-thread-executor-examples/
Java 8 병행 지침서의 첫 시작에 눈독을 들인 것을 환영한다. 이 지침서는 병행 프로그래밍을 자바 8에서 어떻게 수행하는지를 이해하기 쉬운 예제 코드를 이용하여 알려준다. Java Concurrency API를 커버하는 지침서의 여러 시리즈 중 첫번째가 이 글이다. 다음 약 15분동안 어떻게 병렬로 스레드와 작업 및 실행자 서비스를 통해 코드를 실행시키는지 알아보게 된다.
Part 1. 스레드와 실행자(Threads and Executors)
Part 2. 동기화와 락(Synchronization and Locks)
Part 3. 원자적 변수 및 동시성맵(Atomic Variables and ConcurrentMap)
Concurrency API는 Java 5에서 처음 도입되었으며 점진적으로 매 버전 릴리즈마다 보강되었다. 이 글에서 보여질 대부분의 코드 코딩 컨셉은 초기의 자바에서도 물론 적용 가능하지만 이 글에서 제공되는 예제 코드는 Java 8에 집중하여 많은 람다식과 새 문법들이 사용될 것이다. 만일 람다식에 익숙하지 않다면, Java 8 Tutorial을 먼저 보기를 권장한다.
Threads and Runnable
모든 현대 운영체제는 프로세스와 스레드 두 가지를 통해 병렬처리를 지원한다. 프로세스는 서로에게 독립적인 즉, 전형적으로 실행되는 프로그램의 인스턴스이다. 만약 특정 자바 프로그램을 실행시킨다면 운영체제는 새로운 하나의 프로세스를 생성할 것이고 이 프로세스는 다른 프로그램들과 함께 병렬적으로 실행될 것이다. 이 프로세스 내부에서 우리는 스레드를 이용해서 코드를 동시적으로 실행되도록 할 수 있으며 따라서 CPU의 사용가능한 코어들을 최대한 이용할 수 있다.
자바는 JDK 1.0 부터 Threads를 문법적으로 지원한다. 새로운 스레드를 하나 실행시키기 전에 이 스레드가 어떤 코드를 실행시켜야 하는지를 반드시 먼저 알려줘야 한다. 그리고 이를 우리는 작업(task)이라 부른다. 간단히 Runnable - 흔히 기능성 인터페이스(functional interface)라 하는 것들 중 하나인 - 을 구현하기만 하면 된다. 이 인터페이스는 파라미터를 받지 않는 메서드 run() 하나만 가지고 있으며 아래와 같이 구현 가능하다.
public static void main(String[] args) {Runnable task = () -> {String threadName = Thread.currentThread().getName();System.out.println("Hello " + threadName);};task.run();Thread thread = new Thread(task);thread.start();System.out.println("Done!");}
Runnable은 기능성 인터페이스다. 따라서 위 예제처럼 현재 스레드의 이름을 콘솔에 출력하도록 람다를 이용한 코딩이 가능하다. 코드에서 보이듯 메인 스레드에서 직접적으로 runnable을 실행시키고 있으며 이후에 새로 만들어진 스레드에서 runnable이 구동되고 있다.
결과는 예상할 것도 없이,
Hello mainDone!Hello Thread-0
또는
Hello mainHello Thread-0Done!
이 된다.
어느 코드가 언제 실행될지는 예측이 불가능하므로 'Done!'을 출력하는 코드가 먼저 수행될지 나중에 수행될지는 아무도 모른다. 즉, 실행 순서는 비결정적이며 따라서 불확실성이 기본이 되는 상황에서 큰 프로그램을 만들 때 병렬적으로 프로그래밍을 하는 것은 무척이나 머리아픈 일이 된다.
스레드는 특정 기간동안 대기(sleep)할 수 있는데 이를 이용하면 긴시간이 걸리는 작업을 다음 코드와 같이 흉내내 볼 수 있다.
위 코드는 첫 번재 출력과 두 번째 출력 사이에 1초를 쉰다. 여기까지는 다 아는 내용이다. 그러나 이와 같이 Thread를 이용하게 되면 반복적인 코드에 지루할 뿐만 아니라 에러가 나기 쉽다(대부분). 이러한 이유로 인해 2004년 자바 5의 출현 때 Concurrency API가 재소개되었다. 이 java.util.concurrent 패키지에 속한 API들은 매우 유용한 클래스를 가지게 됐다. 이 이후로 매 자바 릴리즈마다 Concurrency API는 보강되었고 Java 8에서는 더 새로운 클래스와 메서드를 이용하여 병렬성을 다룰 수 있게 되었다.Runnable runnable = () -> {try {String name = Thread.currentThread().getName();System.out.println("Foo " + name);TimeUnit.SECONDS.sleep(1);System.out.println("Bar " + name);}catch (InterruptedException e) {e.printStackTrace();}};Thread thread = new Thread(runnable);thread.start();
그런고로 그 API 중 하나인 executor services를 더 자세히 살펴보자.
Executors
Concurrent API에서 ExecutorService라는 개념이 도입됐다. 스레드를 직접적으로 다루는 가장 최상위 API로 앞으론 Thread 대신 이놈을 사용한다. Thread 다음 버전이라 생각하면 된다. Executors는 작업(task)들을 비동기적으로 실행시킬 수 있으며 기본적으로 스레드 풀을 운영한다. 따라서 우리는 스스로 스레드를 만들 필요가 전.혀. 없다. 스레드 풀의 스레드를은 자신의 임무를 다 마친 스레드들을 응당 재사용한다. 그렇기에 하나의 executor service를 이용하여 응용프로그램이 시작하고 끝날 때까지 우리가 원하는 만큼 병렬용 작업을 만들고 실행시킬 수 있다.
아래는 한 예시이다.
ExecutorService executor = Executors.newSingleThreadExecutor();executor.submit(() -> {String threadName = Thread.currentThread().getName();System.out.println("Hello " + threadName);});// => Hello pool-1-thread-1
잘 보면 Executors 가 팩토리메서드를 제공하므로 ExecutorService 의 인스턴스를 얻는 것은 그저 원하는 메서드를 호출하기만 하면 이뤄진다. 위에서는 사이즈 1 크기의 스레드 풀을 갖는 executor를 생성하였다. submit으로 실행시킬 작업을 추가한다.
결과는 위와 유사할 것이지만 차이가 존재한다. 위 프로그램은 절대 종료되지 않는다는 거다. Executors는 반드시 명시적으로 종료시켜야만 한다. 그러지 않으면 다른 작업을 기다리며 종료되지 않는다.
ExecutorService 는 종료를 위한 목적으로 2개의 메서드를 제공하는데 하나는 shutdown()이고 다른 하나는 shutdownNow()이다. 전자는 현재 진행중인 작업이 남아있다면 모두 끝날때까지 기다렸다가 종료시키고 후자는 진행되는 작업이 있건말건 종료시킨다.
아래에 권장되는 executors 종료 방법을 기술하였다.
try {System.out.println("attempt to shutdown executor");executor.shutdown();executor.awaitTermination(5, TimeUnit.SECONDS);}catch (InterruptedException e) {System.err.println("tasks interrupted");}finally {if (!executor.isTerminated()) {System.err.println("cancel non-finished tasks");}executor.shutdownNow();System.out.println("shutdown finished");}
첫째로 executors는 현재 실행중인 작업들을 위해 특정 시간동안 기다려 준다(위에선 5초). 그러다가 인내심이 바닥나면 모든 것을 강제 종료시킨다.
Callables and Futures
친숙한 Runnable과 함께, executors는 다른 종류의 작업인 Callable을 지원한다. 이놈 또한 기능적 인터페이스이고 Runnable과 99% 동일하다. 1%의 차이는 Runnable과 달리 이놈은 리턴 타입을 갖는다.
백문이 불여백견. 1초간 잠을 잔 후에 정수를 리턴하는 Callable을 람다를 이용해 구현한 코드다.
Callable<Integer> task = () -> {try {TimeUnit.SECONDS.sleep(1);return 123;}catch (InterruptedException e) {throw new IllegalStateException("task interrupted", e);}};
Runnalbe처럼 Callable도 executor service에 들어가 실행될 수 있다. 그러면 의문이 생긴다. 리턴 값은 어떻게 받느냐는 거다. 아니 submit은 작업이 끝날때까지 기다려주는 메서드도 아닌데 추가하고 멋대로 실행되게 될 Callable 작업의 리턴 값을 executor service가 제공해 줄 방법이 없지 않은가. 라고 고민해 봐야 헛수고다. 이미 executor가 리턴값을 주기 위해 Future라는 새 친구를 준비해 뒀다. 이 Future로부터 특정 시간의 흐름 이후에 리턴값을 돌려받을 수 있다. 즉 저넘은 리턴 값 가져다 주는 심부름꾼이다.
ExecutorService executor = Executors.newFixedThreadPool(1);Future<Integer> future = executor.submit(task);System.out.println("future done? " + future.isDone());Integer result = future.get();System.out.println("future done? " + future.isDone());System.out.print("result: " + result);
주목해야 할 부분은 submit을 통해 작업을 등록시키고 난 뒤다. isDone() 메서드를 잘 보길 바란다. 심부름꾼에게 우린 Callable이 작업 끝내고 리턴 값 줬냐? 라고 물어보는 것이다. 아직 그렇지 않다면(false) 더 기다렸다가 물어보면 된다. 하지만 위 코드에서 보면 get()을 호출 하고 있다. 이건 심부름꾼에게 이렇게 말하는 것과 같다. 리턴 값 줄때까지 무한정 기다리겠다. 즉, 리턴받을 때까지 코드의 실행은 멈춘다.
끝나면 결과인 123을 가져올 수 있게 된다.
future done? falsefuture done? trueresult: 123
결과다. System.out.println 부분이 1초 이상 걸린다면 위 결과는 안 나올거다(그럴 가능성이 요샌 없다).
Future는 executor가 구동하고 있는 작업과 굉장히 연관이 높기 때문에 아직 종료되지 않은 작업의 리턴값을 기다리는 Future가 있다면 executor를 shutdownNow와 같은 작업으로 종료시키려 할 때 예외가 발생시킨다는 사실을 명심해야 한다. 나 안 끝났는데 왜 종료시키느냐고 따지는 거다.
눈매가 뭉툭한 사람이라면 아마 executor를 만든 방법이 약간 달랐다는 것을 몰랐을 거다. 이번에 새로 사용한 메서드는 newFixedThreadPool(1) 으로 기능상으론 newSingleThreadExecutor()와 같다. 우리가 2 이상의 인자를 넣지 않는다면 말이다.
Timeouts
future.get() 을 해버렸다고 치자. future에 대응한는 Callable 작업이 끝나서 값을 반환하지 않는 이상 컴퓨터에 전기를 끊어주지 않고는 실행을 지속시킬 수 없다. 여기에 카운터어택을 날릴 방법이 있다면 대기할 시간을 get 메서드의 인자로 넣어주는 것이다.
ExecutorService executor = Executors.newFixedThreadPool(1);Future<Integer> future = executor.submit(() -> {try {TimeUnit.SECONDS.sleep(2);return 123;}catch (InterruptedException e) {throw new IllegalStateException("task interrupted", e);}});future.get(1, TimeUnit.SECONDS);
분명 Callable 작업은 2초를 기다리지만 get은 1초밖에 안 기다리겠다고 한다. 결과적으로 TimeoutException이 발생한다.
Exception in thread "main" java.util.concurrent.TimeoutExceptionat java.util.concurrent.FutureTask.get(클라스.java:1024)
왜 이 예외가 발생하는지 원인을 추측했다면 스스로 감탄해도 좋다. 2초 > 1초다.
Executors 는 invokeAll() 메서드를 통해 일괄적으로 다수의 callables을 등록할 수 있다 이 메서드는 callables의 컬렉션을 받아들이고 future들의 리스트를 반환한다.
ExecutorService executor = Executors.newWorkStealingPool();List<Callable<String>> callables = Arrays.asList(() -> "task1",() -> "task2",() -> "task3");executor.invokeAll(callables).stream().map(future -> {try {return future.get();}catch (Exception e) {throw new IllegalStateException(e);}}).forEach(System.out::println);
같은 컬렉션임을 알리기 위해 callables 에 진한 표시 한거는 이해 되는데 리턴 값이라는 리스트라는 future가 왜 저기에 표시되었는지 궁금한 사람은 Java 8의 새로운 stream API를 몰라서 그런거다. 나중에 다룬다. 단지, List 는 .stream() 메서드를 호출할 수 있다고만 알아둬라. 위 코드는 모든 future 리스트를 가져와서 리턴값을 가져와 forEach에서 출력한다.
추가적으로 InvokeAny() 도 있다. 이 메서드는 invokeAll()과는 약간 동작방식이 다르다. 이 메서드는 동일하게 Callable의 컬렉션을 인자로 가져가지만, 오로지 가장 먼저 실행 완료되어 리턴값을 뱉어내는 Callable의 리턴값만을 반환한다.
과연 그런가 확인해보기 위해 Callable들 몇개는 3초 걸리고 하나만 1초 걸리게 만들어 테스트 해보자. 각각 a1, a2, a3 을 리턴하도록 하면 1초가 먼저 끝나니 a2가 리턴되어야 마땅할 것이다.
Callable<String> callable(String result, long sleepSeconds) {return () -> {TimeUnit.SECONDS.sleep(sleepSeconds);return result;};}
즉, 첫번째 인자로 리턴값을 정하고, 두번째 인자로 기다릴 시간을 정하도록 만들었다.
ExecutorService executor = Executors.newWorkStealingPool();List<Callable<String>> callables = Arrays.asList(callable("a1", 3),callable("a2", 1),callable("a3", 3));String result = executor.invokeAny(callables);System.out.println(result);// => a2
오오.. 좋다.
그런데 이번엔 newWorkStealingPool() 이 사용되었다!!(이런 식으로 설명하는 것을 기회주의자식(틈새형) 설명이라 한다). 이넘은 ForkJoinPool 타입의 executor인데, 일반적인 executor와 동작방식이 약간 다르다. 고정된 크기의 풀 사이즈를 사용하는게 아니라 호스트 컴퓨터의 CPU의 가용한 코어의 갯수에 대응하는 크기로 풀 사이즈를 챙겨간다. 이미 Java 7부터 존재했고 이후 Part2, Part 3에서 다룰지도 모른다.
Scheduled Executors
한 executor에 한번 특정 작업을 등록시키고 실행시키는 방법은 알아냈다. 그러면 한번 등록한 작업을 여러번 주기적으로 실행시키고 싶을 땐 어떻게 하는지 알아봐야 한다고 치자. 이때 사용하는 것이 scheduled thread pool이다.
ScheduledExecutorService 는 작업을 주기적으로나 특정 시간이 지나 만료되었을 때 한번 실행시킬 수 있는 능력을 겸비하고 있다.
아래 코드는 한 작업을 3초의 초기 지연 시간이 지난 이후에 실행되도록 예정해 놓는 내용을 담고 있다.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);Runnable task = () -> System.out.println("Scheduling: " + System.nanoTime());ScheduledFuture<?> future = executor.schedule(task, 3, TimeUnit.SECONDS);TimeUnit.MILLISECONDS.sleep(1337);long remainingDelay = future.getDelay(TimeUnit.MILLISECONDS);System.out.printf("Remaining Delay: %sms", remainingDelay);
여기서 새로운 심부름꾼이 등장한다. ScheduledFuture다. getDelay() 를 제공하여 남은 지연을 알려준다. 지연이 지나면 작업은 병렬적으로 실행된다.
주기적으로 실행되게 하려면 executor가 제공하는 2개의 메서드 중 하나를 사용하면 된다. scheduleAtFixedRate()와 scheduleWithFixedDelay()다. 전자는 고정된 시간 비율로 작업을 실행한다. 즉 2초에 1번 실행과 같은 식이다.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);Runnable task = () -> System.out.println("Scheduling: " + System.nanoTime());int initialDelay = 0;int period = 1;executor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.SECONDS);
또한 전자의 메서드는 처음으로 실행되기 전까지 기다릴 시간을 인자로 받을 수 있다.
하나 주의해야 하는 것이 있는데 전자의 메서드는 실행될 작업의 수행시간은 전혀 고려되지 않는다는 점이다. 만약 특정 작업을 2초에 한번씩 실행되도록 해놨는데 작업이 수행되는데 모두 3초가 걸린다고 하면 실행은 다 못했는데 새 작업이 수행되고 스레드 풀은 곧 꽉 차게 된다.
따라서 이러한 경우 고려해야 할 메서드가 후자의 메서드이다. 이놈은 전자의 반대로 행동한다. 즉 수행 시간을 고려한다. 그것이 차이점이며 작업이 끝난 후부터 수행될 시간을 측정한다. 즉 2초에 한번 그러나 3초 걸리는 작업이면 5초에 한번씩 수행될 거다.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);Runnable task = () -> {try {TimeUnit.SECONDS.sleep(2);System.out.println("Scheduling: " + System.nanoTime());}catch (InterruptedException e) {System.err.println("task interrupted");}};executor.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);
이 경우 초기 실행 지연은 0이며 수행시간 2초고 매순간 고정 지연은 1초다. 따라서 0초 3초 6초 9초 ... 마다 작업이 실행될 것이다.
2015년 5월 31일 일요일
[일기] chkconfig를 이용한 Redhat에서 init 서비스 비활성화.
Enabling and disabling services during start up in GNU/Linux
원본 링크
리눅스에서 부팅 시 초기화할 서비스의 on/off를 어떻게 하는지 알아보던 중 알게된 굉장한 링크.
Redhat에서는 chkconfig 를 이용하며, 다음과 같이 사용한다.
[출처는 위에 기재]
As an example, lets enable the Apache web server to start in run levels 2, 3, and 5. This is how it is done.
We first add the service using
This will enable the Apache web server to automatically start in the run levels 2, 3 and 5. You can check this by running the command -
Red Hat/Fedora also have a useful script called
and to stop the service...
The options being start, stop and restart which are self explanatory.
원본 링크
리눅스에서 부팅 시 초기화할 서비스의 on/off를 어떻게 하는지 알아보던 중 알게된 굉장한 링크.
Redhat에서는 chkconfig 를 이용하며, 다음과 같이 사용한다.
[출처는 위에 기재]
How to enable a service
As an example, lets enable the Apache web server to start in run levels 2, 3, and 5. This is how it is done.
We first add the service using
chkconfig
script. Then turn on the service at the desired run levels.# chkconfig httpd --add # chkconfig httpd on --level 2,3,5
This will enable the Apache web server to automatically start in the run levels 2, 3 and 5. You can check this by running the command -
# chkconfig --list httpd
How to disable a service
# chkconfig httpd off # chkconfig httpd --del
Red Hat/Fedora also have a useful script called
service
which can be used to start or stop any service. For example, to start Apache web server using the service
script, the command is as follows -# service httpd start
and to stop the service...
# service httpd stop
The options being start, stop and restart which are self explanatory.
2015년 5월 13일 수요일
[일기] 기분나쁜 일기장 HARD Constraints (7.1 ~ 7.2)
7. Hard Constraints ( 중 제약;? )
지난 장에서 우리는 스프링들을 살펴보았고 만들었던 힘 발생기와, 용수철에 여러 물체를 연결시켰던 경우는 힘을 단지 다른 한 물체에만 적용시켰어야만 했음을 확인했다. 이번에는 다른 물체들의 움직임에 기반을 두어 행동하는 물체를 다룰 것이다.
비록 용수철이 많은 상황을 표현하기 위해 사용될 수 있지만, 그들은 최악의 방식으로 행동하기도 한다. 우리가 만일 두 물체가 꽉 붙어서 이동하기를 원한다고 할때, 우리가 원하는 용수철 상수는 현실적으로 구현하기 불가능하다. 견고한, 딱딱한 막대에 의해서 연결되어있는 물체들을 시연하고자 하는 경우나 단단한 표면에 의해 거리를 유지하는 물체들을 시연하고자 할 때, 용수철은 실행 가능한 옵션이 아니다.
이번 장에서는 우리는 강력한 제약에 대해 이야기할 것이다. 초기에 가장 흔하게 마주치는 제약을 살필 것이다 - 충돌과 물체간 접촉. 즉, 이전에 쓰인 수식들이 동일하게 다른 여러 종류의 제약들에 사용될 수 있는데, 막대나 늘어나지 않는 케이블이 그 예이고, 이들은 물체들을 함께 연결시키는 데 쓰일 수 있다.
이러한 제약을 물리 엔진에서 극복하기 위해, 힘 발생기의 평온한 세계로부터 떠나야 한다. 모든 우리가 이 책에서 다루는 엔진은 힘 발생기와는 다른 방식으로 제약을 처리한다. 책의 막바지 부분에서는 가령 18장에서, 우리는 대안적인 접근을 브리핑 할 것이며 하나로 통합할 것이다.
7.1 Simple Collision Resolution ( 단순 충돌 해결 )
이러한 제약을 극복하기 위해, 우리는 충돌 해결 시스템을 우리의 엔진에 추가해야 한다.
이 책의 진행을 위해서, '충돌'은 두 물체가 접촉하는, 겹치는 어떠한 상황이라도 부르는 용어로 정의한다. 영문 그 자체의 의미에서 우리는 충돌을 격렬한 절차로 이해하는 것이 보통이다. 이 경우 매우 큰 접근 속도(closing velocity;?)로 물체들은 닿는다. 우리의 의도상 이 또한 분명 사실이나, 두 물체가 단순히 접촉(touching)하는 것 즉, 거의 없는 접근 속도로(0이라도) 도달한다 할지라도 충돌로 생각하기로 하는 것이다. 비충돌접촉(resting contact)을 해결할 때도 빠른 속도로 충돌하는 물체들의 충돌을 해결할 때 쓰이는 절차와 같은 방식을 사용할 것이다. 따라서 위의 충돌의 의미 가정은 지당할 정도로 중요하다. 이 장의 마지막 부분에서 다양한 관점으로 깊숙이 살펴볼 것이다. 나중에 용어를 변경하는 것을 피하기 위해서, 이 장에서는 충돌(collision)과 접촉(contact) 두 용어를 서로 같은 것으로 간주하여 진행하기로 한다.
두 물체가 충돌했을 때, 충돌 후의 물체들의 운동은 충돌 전의 물체들의 운동으로부터 계산할 수 있다. 이를 충돌 해결(collision resolution)이라 한다. 여기서 우리는 충돌 후 가능한 운동양상을 물체가 정확히 따르도록 하여 충돌을 해결한다. 충돌은 우리가 관찰할 수 없으리만큼 극히 짧은 시간동안에 발생하기 때문에 우리는 그 시간 안에서, 각 물체의 운동을 직접적으로 조작한다.
모든 종류의 사건은 압축이 진행되는 동안 발생할 수 있으며 관련된 물질의 특성은 매우 복잡한 상호작용을 일으킬 수 있다. 그러나 현실에서는 나타나는 물체의 움직임은 감쇠 용수철의 행동과 동일하지 않고 컴퓨터로는 현실에서 나타나는 현상을 섬세하게 포착해 낼 수 없다.
특히 용수철 모델은 충돌하는 동안 운동량이 보존된다는 가정을 하고 있다.
[식 7.3]
여기서 Ma는 물체 a의 질량이며, pa는 물체 a의 충돌 전 속도이고 pa'는 충돌 후 속도이다.
다행히도 충돌의 방대한 대다수는 이상적인 용수철과 같이 행동한다. 따라서 운동량 보존을 가정함으로써 신뢰할만한 물체의 행동방식을 완벽하게 만들어(알아낼)낼 수 있다. 여기서 식 [7.3]을 충돌처리 모델로 사용할 것이다.
식 [7.3]을 통해 총체적으로 충돌 전후의 속도를 파악할 수는 있으나 각각의 개별 속도까지알아내는 것은 불가능하다. 각각의 속도들의 관계는 접근 속도라는 개념으로 서로 연결되어 있다. 수식에 따르면
[수식 Vs = -cvs]
이고, 여기서 Vs'는 충돌 후의 분리속도를 뜻하고 Vs는 충돌 전의 분리 속도를 의미한다. 그리고 이 식에서 나타난 c가 바로 반발 계수이다.
반발 계수를 통해 충돌 후에 물체들이 어떤 속력으로 멀어지게 될지를 결정할 수 있다. 충돌 때의 물질의 재질에 따라 다르다. 서로 맏닿는 재질의 쌍에 따라 반발 계수는 달라진다. 당구공이나 테니스 공과 같은 몇몇 물체들은 큐와 라켓에서 튕겨져 나간다. 눈덩이를 사람 얼굴에 던지면 즉, 이런 다른 이러한 종류의 물체들은 충동했을 때 서로 들러붙게 된다.
반발계수가 1이면 충돌하는 물체들은 그들이 서로에게 접근해 오던 속도와 같은 크기의 속력을 갖고 서로 튕겨나가게 된다. 반발계수가 0이면 두 물체는 접착하고 함께 여행한다. 즉, 둘의 분리 속도는 0이다. 반발계수와 상관없이 식 [7.3]은 양변의 운동량이 같음을 보장한다.
두 식을 사용하면 우리는 pa'와 pb'의 값을 알아낼 수 있다.
입자가 땅과 충돌할 때는 a 물체만 있고 b 물체는 없는 것과 같다. 이 경우 a의 관점에서 충돌법선은 다음과 같이 된다.
[충돌법선식]
이 때, 충돌점이 지면과 같은 높이에 있다고 가정한다.
입자들이 완전한 강체들과 함께 운동하도록 두었을 때는 명시적인 충돌 법선을 갖는 것이 매우 중요해진다. 물체 내부 충돌을 위해서 특히 그렇다. 책의 나머지 부분을 예습하지 않은 경우 그림 [7.1]를 보고 어떤 상황을 우리가 고려하게 될지 대략적으로 알 수 있게 될 것이다. 그림에는 두 물체가 충돌하고 있으며 각 물체의 모양 덕분에 만일 그들의 위치만을 고려해서 계산했을 경우 얻게 될 충돌 법선과는 전혀 반대 방향을 갖는 충돌 법선을 갖는다. 즉 위치만을 고려해서 계산하면 풀릴 수 없다. 물체들의 아치형 부분이 겹쳤으며 따라서 충돌로 인해 두 물체는 서로 같이 붙어 있게 된다기 보단 서로 멀어지지 못하는 상황에 처하게 된다. 이 장의 마지막 부분에서 입자에 대한 이와 유사한 상황을 살펴볼 것이고 그 상황은 막대나 다른 단단한 연결을 표현하는데 나타날 것이다.
올바른 법선 벡터를 구하는 식은 다음과 같다.
[식 7.4]
[그림 7.1]
구조체는 충돌에 관련되어 있는 각각의 물체에 대한 포인터와 충돌에 대한 반발계수를 보유한다. 그리고 충돌 법선의 경우 첫번째 물체 관점에서 기술된다. 만일 한 물체와 풍경 사이의 충돌을 처리하고 있다면(즉 물체가 하나만 관련되는 경우), 두 번째 물체를 가리키는 포인터의 값은 NULL이 된다.
충돌을 해결하기 위해서 이 섹션의 앞부분에서 살핀 충돌에 관련된 수식을 적용할 것이다.
- pcontacts.h 발췌 -
---------------------------------------------------------------------
class ParticleContact {
///... 전에 기술한 부분
protected :
/// 충돌을 해결한다. 속도와 관통을 둘다 해결한다.
void resolve(real duration);
/** 분리 속도를 계산한다 */
real calculateSeparatingVelocity() const;
private:
/** 충격량을 다룬다 */
void resolveVelocity(real duration);
};
---------------------------------------------------------------------
- pcontacts.cpp 발췌 -
---------------------------------------------------------------------
#include <cyclone/pcontacts.h>
void ParticleContact::resolve(real duration) {
resolveVelocity(duration);
}
real ParticleContact::calculateSeparatingVelocity() const {
Vector3 relativeVelocity = particle[0]->getVelocity();
if(particle[1]) relativeVelocity -= particle[1]->getVelocity();
return relativeVelocity * contactNormal;
}
void ParticleContact::resolveVelocity(real duration) {
/** 충돌 방향의 속도를 구한다 */
real separatingVelocity = calculateSeparatingVelocity();
/** 해결될 필요가 있는지 확인한다 */
if( separatingVelocity > 0 ) {
// 충돌이 분리되는 중이거나 고정되어 움직이지 않으면 충격량 적용 필요 없음
return;
}
// 다시 새 분리 속도를 계산한다.
real newSepVelocity = -separatingVelocity * restitution;
real deltaVelocity = newSepVelocity - separatingVelocity;
real totalInverseMass = particle[0]->getInverseMass();
if( particle[1]) totalInverseMass += particle[1]->getInverseMass();
//무한 질량이면 무시한다.
if( totalInverseMass <= 0 ) return;
real impulse = deltaVelocity / totalInverseMass;
//역질량 단위의 충격량 크기 계산
Vector3 impulsePerIMass = contactNormal * impulse;
//충격량 적용
particle[0]->setVelocity(particle[0]->getVelocity() + impulsePerIMass * particle[0]->getInverseMass());
if(particle[1]) {
particle[1]->setVelocity(particle[1]->getVelocity() + impulseIMass * -particle[1]->getInverseMass());
}
}
---------------------------------------------------------------------
이로 인해 두 물체의 속도가 직접적으로 바뀌고 충돌이 반영된다.
두 물체가 충돌했을 때, 충돌 후의 물체들의 운동은 충돌 전의 물체들의 운동으로부터 계산할 수 있다. 이를 충돌 해결(collision resolution)이라 한다. 여기서 우리는 충돌 후 가능한 운동양상을 물체가 정확히 따르도록 하여 충돌을 해결한다. 충돌은 우리가 관찰할 수 없으리만큼 극히 짧은 시간동안에 발생하기 때문에 우리는 그 시간 안에서, 각 물체의 운동을 직접적으로 조작한다.
7.1.1 The Closing Velocity ( 접근 속도 )
충돌하는 강체들의 운동에 영향을 주는 법칙에는 그 강체들의 접근 속도가 반드시 고려된다. 'Closing Velocity'란 속도의 합으로써(상대 속도), 두 물체가 함께 움직일 때의 속도(서로의 상대적인)이다.
이것은 속력 대신 속도임을 명심하라. 이 값이 아무리 스칼라량을 갖는다 하더라도 속도임을 잊지 말길 바란다. 속력(speed)은 방향이 없다. 이것은 단지 양의 값만을 갖는다. 속도는 방향을 가질 수 있다. 이때 어떤 스칼라 값이 있다면 방향은 값의 부호에 의해 결정된다. 그리고 서로 떨어져서(멀어지며) 이동하는 물체들은 0보다 작은 접근 속도를 갖게 된다(가까워지지 않는다).
우리는 두 물체의 접근 속도를 각 한 물체의 속도 중 한 물체에서 다른 물체 방향으로의 속도 성분을 찾아냄으로써 계산해 낸다. 즉,
이고, Vc는 접근 속도를 뜻하며 스칼라 값을 갖는다. Pa와 Pb는 각각 물체 a와 b의 위치를 나타내고 점(·) 표기는 내적을 뜻한다. 그리고 p^은 p 벡터와 같은 방향으로의 단위 길이 벡터를 의미한다. 위 식은 다음과 같이 정리될 수 있다.
비록 관습일 뿐이지만, 수식의 부호를 일부로 바꾸는 경우가 많다. 즉 이 의미는 접근 속도보다는 분리 속도(separating velocity)에 관심을 갖는다는 의미이다. 접근 속도는 한 물체가 다른 물체에 대한 상대적인 접근 속도를 의미하는 것이고 방향은 두 물체의 사이이다.
두 물체가 서로에 대해 점점 다가가고 있는 경우에는 음의 상대 속도를 얻게 된다. 그리고 서로 멀어지는 물체에 대해서는 양의 속도를 얻게 된다. 수학적으로 이것은 단지 식 [7.1]의 부호 변환 문제일 뿐 별다를 바가 없다.
여기서 Vs는 분리 속도를 의미하는데 이 책의 나머지에서 사용될 포맷이다. 물론 좋아한다면 계속 접근 속도를 고수해도 좋다. 비록 당신이 엔진에서 이를 벌충하기 위해 변수 수량의 부호를 반드시 뒤집어야 하겠지만, 이는 단지 선호의 문제일 뿐이다.
7.1.2 The Coefficient Of Restitution ( 반발 계수 )
지난 마지막 장에서 봤었듯이, 두 물체가 충돌할 때 그들은 서로 함께 압축된다. 그리고나서 표면의 용수철같은 변형은 두 물체를 분리시키기 위한 힘을 유발시킨다. 이 모든 것이 매우 짧은 시간의 간격 내에서 이루어진다(너무 짧아서 프레임마다 실행할 수 없다-물론 매우 빠른 속도의 영화필름에서는 충분한 길이일 것임에도 불구하고). 결국 두 물체는 더 이상 어떠한 접근 속도도 갖지 않게 된다. 비록 움직임이 용수철같았으나, 현실에서는 더욱 유사하다.모든 종류의 사건은 압축이 진행되는 동안 발생할 수 있으며 관련된 물질의 특성은 매우 복잡한 상호작용을 일으킬 수 있다. 그러나 현실에서는 나타나는 물체의 움직임은 감쇠 용수철의 행동과 동일하지 않고 컴퓨터로는 현실에서 나타나는 현상을 섬세하게 포착해 낼 수 없다.
특히 용수철 모델은 충돌하는 동안 운동량이 보존된다는 가정을 하고 있다.
[식 7.3]
여기서 Ma는 물체 a의 질량이며, pa는 물체 a의 충돌 전 속도이고 pa'는 충돌 후 속도이다.
다행히도 충돌의 방대한 대다수는 이상적인 용수철과 같이 행동한다. 따라서 운동량 보존을 가정함으로써 신뢰할만한 물체의 행동방식을 완벽하게 만들어(알아낼)낼 수 있다. 여기서 식 [7.3]을 충돌처리 모델로 사용할 것이다.
식 [7.3]을 통해 총체적으로 충돌 전후의 속도를 파악할 수는 있으나 각각의 개별 속도까지알아내는 것은 불가능하다. 각각의 속도들의 관계는 접근 속도라는 개념으로 서로 연결되어 있다. 수식에 따르면
[수식 Vs = -cvs]
이고, 여기서 Vs'는 충돌 후의 분리속도를 뜻하고 Vs는 충돌 전의 분리 속도를 의미한다. 그리고 이 식에서 나타난 c가 바로 반발 계수이다.
반발 계수를 통해 충돌 후에 물체들이 어떤 속력으로 멀어지게 될지를 결정할 수 있다. 충돌 때의 물질의 재질에 따라 다르다. 서로 맏닿는 재질의 쌍에 따라 반발 계수는 달라진다. 당구공이나 테니스 공과 같은 몇몇 물체들은 큐와 라켓에서 튕겨져 나간다. 눈덩이를 사람 얼굴에 던지면 즉, 이런 다른 이러한 종류의 물체들은 충동했을 때 서로 들러붙게 된다.
반발계수가 1이면 충돌하는 물체들은 그들이 서로에게 접근해 오던 속도와 같은 크기의 속력을 갖고 서로 튕겨나가게 된다. 반발계수가 0이면 두 물체는 접착하고 함께 여행한다. 즉, 둘의 분리 속도는 0이다. 반발계수와 상관없이 식 [7.3]은 양변의 운동량이 같음을 보장한다.
두 식을 사용하면 우리는 pa'와 pb'의 값을 알아낼 수 있다.
7.1.3 The Collision Direction And The Contact Normal ( 충돌방향과 충돌법선 )
지금까지 두 물체 사이의 충돌에 관해 다뤘다. 하지만 종종 물리적인 시연을 하지 않는 것들과 한 물체간의 충돌도 지원해야 하는 경우도 있다. 이러한 물리적 시연의 대상이 아닌 것들은 지면(바닥)이거나 한 층의 장벽과 같은 움직이지 않는 물체들이 될 수 있다. 이러한 것들은 질량이 무한한 물체로 간주할 수 있으나 물체로 간주하고 충돌에 관한 속도를 계산하는 것은 어차피 움직이지 않을 물체에 대한 속도를 계산하는 것과 마찬가지이므로 시간 낭비다.
만일 움직이지 않는 풍경적(?) 요소의 일부와 한 물체 사이의 충돌이 일어난다면, 각 물체의 위치를 기반으로 한 벡터를 고려하여 분리 속도를 개산할 수가 없다. 단지 한 물체 정보만을 알기 때문이다. 다시 말하면 우리는 식 [7.2]의 pa-pb 식을 계산할 수가 없다. 따라서 이 수식을 변경할 필요가 있다.
이 pa-pb 표기는 분리 속도의 방향을 알려준다. 분리 속도는 두 물체와 저 표기의 내적을 통해 계산된다. 따라서 두 물체 정보를 갖지 않으면 그 방향을 명시적으로 지정해 줘야 한다. 이 방향이 두 물체가 충돌하고 있는 방향이며 대개 충돌 법선(collision normal or contact normal)이라 불린다. 이들은 방향을 나타내기 때문이고 이 백터의 크기는 항상 1이다.
두 입자가 충돌하고 있는 경우에, 충돌 법선은 다음과 같이 주어질 것이다.
관례상, 충돌 법선은 물체 A의 관점에서 계산한다. 이 경우 즉 a의 관점에서, 충돌은 b에서 a 방향이다. 따라서 pa-pb이다. b의 관점에서 충돌의 방향을 알고자 하는 경우 단순히 -1만 곱해주면 된다. 실제로는 우리는 이러한 일을 명시적으로 하지는 않지만 코드 내의 이러한 값의 반전은 b의 분리 속도를 계산하기 위해 사용되곤 했다. 아마 이 장의 마지막에 구현할 코드에서 이 요소가 있음을 알아차릴 것이다. : 마이너스 부호가 b의 계산에서 나타난다.
입자가 땅과 충돌할 때는 a 물체만 있고 b 물체는 없는 것과 같다. 이 경우 a의 관점에서 충돌법선은 다음과 같이 된다.
[충돌법선식]
이 때, 충돌점이 지면과 같은 높이에 있다고 가정한다.
입자들이 완전한 강체들과 함께 운동하도록 두었을 때는 명시적인 충돌 법선을 갖는 것이 매우 중요해진다. 물체 내부 충돌을 위해서 특히 그렇다. 책의 나머지 부분을 예습하지 않은 경우 그림 [7.1]를 보고 어떤 상황을 우리가 고려하게 될지 대략적으로 알 수 있게 될 것이다. 그림에는 두 물체가 충돌하고 있으며 각 물체의 모양 덕분에 만일 그들의 위치만을 고려해서 계산했을 경우 얻게 될 충돌 법선과는 전혀 반대 방향을 갖는 충돌 법선을 갖는다. 즉 위치만을 고려해서 계산하면 풀릴 수 없다. 물체들의 아치형 부분이 겹쳤으며 따라서 충돌로 인해 두 물체는 서로 같이 붙어 있게 된다기 보단 서로 멀어지지 못하는 상황에 처하게 된다. 이 장의 마지막 부분에서 입자에 대한 이와 유사한 상황을 살펴볼 것이고 그 상황은 막대나 다른 단단한 연결을 표현하는데 나타날 것이다.
올바른 법선 벡터를 구하는 식은 다음과 같다.
[식 7.4]
[그림 7.1]
7.1.4 Impulses ( 충격량 )
우리가 충돌을 해결하기 위해 변경해야 하는 것은 물체들의 속도뿐이다. 지금까지 이 물리 엔진에서 가속도를 이용해 속도의 값들만을 바꿔왔다. 고로 가속도가 오랜 시간 적용되면 속도에는 큰 변화가 생기게 되었다. 여기선 허나 변화는 순간적이다. 속도는 즉각적으로 새 값을 갖게 된다.
힘이 물체의 가속도를 바꾼다는 사실을 회상해보자. 만일 우리가 일시적으로 힘을 바꾼다면, 가속도도 순간적으로 바뀌게 된다. 또한 물체의 속도를 바꾸기 위해서도 같은 방식을 물체에 적용할 수 있다는 생각을 해볼 수 있다. 힘이라기보다는 충격량이라고 불리는 것을 통해서다. 충격량은 속도의 즉각적인 변화이다. 힘의 공식에서와 같은 방식으로
우리는 다음을 생각할 수 있다.
여기서 g는 충격량이다. 충격량은 종종 문자 p를 이용해 표기되지만 여기서는 물체의 위치를 나타내는 p와의 혼동을 피하기 위해 문자 g를 사용한다.
유사한 형태의 두 식이지만 힘과 충격량 사이에는 큰 차이가 있다. 한 물체는 물체에 가해지는 힘이 없는 경우 가속도를 갖지 않는다. D'Alembert의 이론을 이용해서 힘의 합력을 통해 알짜힘에 대한 가속도를 구할 수 있음을 알 것이다. 이와는 반대로 한 물체는 물체에 어떠한 충격량이 작용하지 않는다고 하더라도 속도를 계속해서 갖는다. 따라서 충격량은 속도를 변화시키는 기능을 함을 알수 있다. 물론 충격량이 속도에 대해 완전한 책임이 있는 것은 아니다. 충격량 또한 D'Alembert의 이론을 사용하여 합할 수 있으나, 결과는 속도의 변화로 나타날 것이다. 전체 속도가 아님을 명심해야 한다.
g1 ... g2 는 물체에 작용하는 충격량들의 집합이다. 실제로 우리는 힘에서 했던 것처럼 충격량을 누적하지는 않는다. 단지 충돌 해결 절차(?;collision resolution process) 동안에만 충돌량이 발생하므로 이들을 그 기간동안만 물체에 적용할 뿐이다. 각각은 한 순간에 다음과 같은 식으로 적용될 수 있다.
충돌 해결의 결과는 각 물체에 적용해야할 충격량의 값이 된다. 충격량은 즉시 적용되며 당장에 물체의 속도를 변화시킨다.
7.2 Collision Processing ( 충돌 처리 )
충돌을 다루기 위해 코드 몇 줄을 작성할 것이다. 클래스는 ContactResolver로 명명한다. 이 클래스는 충돌들의 집합을 보유하고 관련된 물체들에 관련된 충격량을 적용시키는 일을 맡을 것이다. 각 충돌은 Contact라는 데이터 구조를 통해 관리할 것이며 다음과 같이 코딩해 보도록 한다.
---- pcontacts.h 발췌 ----
---------------------------------------------------------------------
/**
* @details 충돌은 두 물체가 접촉하고 있음을 나타낸다. 충돌을 해결하는 것은 이들의 교차(겹침)를 없애는 것을 목적으로 한다. 따라서 충분한 충격량을 적용하여 그들을 떨어뜨려 놓는다. 충돌한 강체는 튀어오를 수 있다.
* 충돌에 호출가능한 함수는 없으며, 단지 충돌의 상세를 보유한다. 충돌의 집합을 해결하기 위해선 입자 충돌 해결사(?) 클래스를 사용한다.
*/
class ParticleContact {
/// 충돌에 관련된 입자들에 대한 포인터이며 풍경요소와 충돌할 경우 두번째 포인터는 NULL 값을 가질 수 있다.
Particle* particle[2];
/// 충돌에 대한 반발 계수
real restituion;
/// 계 좌표계에서 충돌의 방향
Vector3 contactNormal;
};
* 충돌에 호출가능한 함수는 없으며, 단지 충돌의 상세를 보유한다. 충돌의 집합을 해결하기 위해선 입자 충돌 해결사(?) 클래스를 사용한다.
*/
class ParticleContact {
/// 충돌에 관련된 입자들에 대한 포인터이며 풍경요소와 충돌할 경우 두번째 포인터는 NULL 값을 가질 수 있다.
Particle* particle[2];
/// 충돌에 대한 반발 계수
real restituion;
/// 계 좌표계에서 충돌의 방향
Vector3 contactNormal;
};
---------------------------------------------------------------------
구조체는 충돌에 관련되어 있는 각각의 물체에 대한 포인터와 충돌에 대한 반발계수를 보유한다. 그리고 충돌 법선의 경우 첫번째 물체 관점에서 기술된다. 만일 한 물체와 풍경 사이의 충돌을 처리하고 있다면(즉 물체가 하나만 관련되는 경우), 두 번째 물체를 가리키는 포인터의 값은 NULL이 된다.
충돌을 해결하기 위해서 이 섹션의 앞부분에서 살핀 충돌에 관련된 수식을 적용할 것이다.
- pcontacts.h 발췌 -
---------------------------------------------------------------------
class ParticleContact {
///... 전에 기술한 부분
protected :
/// 충돌을 해결한다. 속도와 관통을 둘다 해결한다.
void resolve(real duration);
/** 분리 속도를 계산한다 */
real calculateSeparatingVelocity() const;
private:
/** 충격량을 다룬다 */
void resolveVelocity(real duration);
};
---------------------------------------------------------------------
- pcontacts.cpp 발췌 -
---------------------------------------------------------------------
#include <cyclone/pcontacts.h>
void ParticleContact::resolve(real duration) {
resolveVelocity(duration);
}
real ParticleContact::calculateSeparatingVelocity() const {
Vector3 relativeVelocity = particle[0]->getVelocity();
if(particle[1]) relativeVelocity -= particle[1]->getVelocity();
return relativeVelocity * contactNormal;
}
void ParticleContact::resolveVelocity(real duration) {
/** 충돌 방향의 속도를 구한다 */
real separatingVelocity = calculateSeparatingVelocity();
/** 해결될 필요가 있는지 확인한다 */
if( separatingVelocity > 0 ) {
// 충돌이 분리되는 중이거나 고정되어 움직이지 않으면 충격량 적용 필요 없음
return;
}
// 다시 새 분리 속도를 계산한다.
real newSepVelocity = -separatingVelocity * restitution;
real deltaVelocity = newSepVelocity - separatingVelocity;
real totalInverseMass = particle[0]->getInverseMass();
if( particle[1]) totalInverseMass += particle[1]->getInverseMass();
//무한 질량이면 무시한다.
if( totalInverseMass <= 0 ) return;
real impulse = deltaVelocity / totalInverseMass;
//역질량 단위의 충격량 크기 계산
Vector3 impulsePerIMass = contactNormal * impulse;
//충격량 적용
particle[0]->setVelocity(particle[0]->getVelocity() + impulsePerIMass * particle[0]->getInverseMass());
if(particle[1]) {
particle[1]->setVelocity(particle[1]->getVelocity() + impulseIMass * -particle[1]->getInverseMass());
}
}
---------------------------------------------------------------------
이로 인해 두 물체의 속도가 직접적으로 바뀌고 충돌이 반영된다.
피드 구독하기:
글 (Atom)