13
Introduction to Fork Join Framework 1 번역자 : 시스포유 I&C 김형석 수석 작성일 : 2013 년 2 월 8 일 원문 : http://java.amitph.com/2012/08/introduction-to-fork-join-framework.html 드웨어의 지속적인 발전으로 인해, 이제 핸드폰이나 노트북과 같은 우리가 일상 생 활에서 사용하는 대부분의 장치들에 멀티코어 프로세서가 탑재되는 것이 너무나 당연하 게 여겨지는 시대가 되었다. 멀티코어 프로세서들은 개별 코어들이 여러 가지 작업을 병 렬적으로 처리할 수 있도록 설계되어 있으며, 이로 말미암아 프로그래머들은 하드웨어의 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어 프로그래밍을 고려해 야 하는 상황이 되었다. 이 글에서 소개하는 내용은, Java가 최근에 공개한 Fork Join 프레임워크에 대한 것이다. Fork Join 프레임워크는 워크 스틸링 알고리즘 2 을 기반으로 구현되었는데, 하드웨어의 멀 티코어 기능을 효율적으로 이용할 수 있도록 해 주고, 또 스레드(Thread)의 성능을 향상 시키는 기능을 포함하고 있다. 전체적인 이해를 돕기 위해 바로 Fork Join 프레임워크를 이용하는 예제를 언급하는 대신에, 먼저 Java의 기본적인 멀티스레딩 개념과 기법에 대해 언급할 것이다. 그 다음에 Java Executor 프레임워크를 다룰 것인데, Executor, Executor Service, Executor ThreadPool, Callable, Future 등에 대한 개념이 HelloWorld 어플리케이 션을 이용하여 설명될 것이다. 마지막으로 Fork Join 프레임워크와 그에 대한 이해를 돕 기 위한 예제 코드를 상세히 다룰 것이다. 이 글을 모두 읽고 나면, 독자들은 Fork Join 프레임워크와 Executor 프레임워크의 개념과 각각의 차이점을 이해할 수 있게 될 것이 다. 3 1 사전적인 의미로, fork는 분기를 join은 합병을 의미한다. 2 Work Stealing Algorithm. 굳이 우리 말로 번역한다면 작업 재배치 알고리즘 정도가 될 것이다. 로딩된 프로세서에서 유휴 상태에 있는 프로세서로 태스크를 이동시키는 알고리즘을 의미한다.

Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

  • Upload
    others

  • View
    0

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

Introduction to Fork Join Framework1

번역자 : 시스포유 I&C 김형석 수석

작성일 : 2013년 2월 8일

원문 : http://java.amitph.com/2012/08/introduction-to-fork-join-framework.html

하드웨어의 지속적인 발전으로 인해, 이제 핸드폰이나 노트북과 같은 우리가 일상 생

활에서 사용하는 대부분의 장치들에 멀티코어 프로세서가 탑재되는 것이 너무나 당연하

게 여겨지는 시대가 되었다. 멀티코어 프로세서들은 개별 코어들이 여러 가지 작업을 병

렬적으로 처리할 수 있도록 설계되어 있으며, 이로 말미암아 프로그래머들은 하드웨어의

성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어 프로그래밍을 고려해

야 하는 상황이 되었다.

이 글에서 소개하는 내용은, Java가 최근에 공개한 Fork Join 프레임워크에 대한 것이다.

Fork Join 프레임워크는 워크 스틸링 알고리즘2을 기반으로 구현되었는데, 하드웨어의 멀

티코어 기능을 효율적으로 이용할 수 있도록 해 주고, 또 스레드(Thread)의 성능을 향상

시키는 기능을 포함하고 있다. 전체적인 이해를 돕기 위해 바로 Fork Join 프레임워크를

이용하는 예제를 언급하는 대신에, 먼저 Java의 기본적인 멀티스레딩 개념과 기법에 대해

언급할 것이다. 그 다음에 Java Executor 프레임워크를 다룰 것인데, Executor, Executor

Service, Executor ThreadPool, Callable, Future 등에 대한 개념이 HelloWorld 어플리케이

션을 이용하여 설명될 것이다. 마지막으로 Fork Join 프레임워크와 그에 대한 이해를 돕

기 위한 예제 코드를 상세히 다룰 것이다. 이 글을 모두 읽고 나면, 독자들은 Fork Join

프레임워크와 Executor 프레임워크의 개념과 각각의 차이점을 이해할 수 있게 될 것이

다.3

1 사전적인 의미로, fork는 분기를 join은 합병을 의미한다.

2 Work Stealing Algorithm. 굳이 우리 말로 번역한다면 작업 재배치 알고리즘 정도가 될 것이다.

로딩된 프로세서에서 유휴 상태에 있는 프로세서로 태스크를 이동시키는 알고리즘을 의미한다.

Page 2: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

THE OLDER APPROACH

Java 언어가 처음 도입되었을 때부터, Java는 기본적으로 스레드라는 구조를 이용하여 동

시(concurrency) 작업을 지원할 수 있었다. Java의 병렬 프로그래밍 기법은 프로그래머들

이 직접 필요한 스레드를 직접 생성할 수 있도록 허용하였고, 그렇게 개발된 프로그램들

은 동시에(in a concurrent fashion) 실행되었다. 아래 코드는 스레드를 이용하는 가장 단

순한 Java 멀티스레딩 기법의 예이다.

1. new Thread (new Runnable(){

2. public void run(){

3. //Tasks to be performed on this thread

4. }

5. }).start();

이 과정에서 필요한 여러 가지 작업들, 즉, 스레드의 생성, 스레드풀에 대한 관리, 스레드

의 라이프 사이클에 대한 관리, 그리고 스레드간 커뮤니케이션 처리와 같은 일들은 온전

히 프로그래머의 몫이었다. 비록 스레드의 라이프 사이클을 관리할 수 있는 메소드와

wait, notify, notifyAll과 같이 스레드의 동시성을 제어할 수 있는 여러 메소드들이 제공되

기는 했지만, 그렇다고 하더라도 오류 없는 코드를 작성하는 일은 정말 어려운 일이었다.

때로, 다른 스레드가 점유하고 있는 락(lock)이 해제되기를 기다리던 스레드가 멈춰버리

는 일이 자주 일어난다. 예를 들어, 생성자/소비자 패턴에서 생성자는 소비자의 큐

(Queue)가 가득 차서 이것이 해결되기를 기다리고 있고, 소비자는 생성자가 새로운 정보

를 전달해주기를 기다리며 기다리고 있다면 이 두 스레드는 영원히 서로를 기다리며 멈

춰있을 것이다.4 이런 종류의 문제는 디버그하기도, 그리고 고치기도 어렵다.

3 이 글을 무리 없이 읽기 위해서는, Java 스레드 프로그래밍에 대한 기본적인 이해가 필요하다.

Thread, Runnable, synchronized, wait, notify, notifyAll 등에 대한 이해가 없으면 쉽게 이해하기 어

려울 수 있다.

4 이러한 상태를 데드락(deadlock) 상태라고 한다. 스레드가 정상적으로 동작하지 못하고 멈춰버

리는 현상의 원인은 데드락 외에도 고갈(Starvation 혹은 Resource Starvation)에 의한 것일 수도

있다. 데드락과 고갈에 대한 상세한 내용은 http://en.wikipedia.org/wiki/Deadlock과

http://en.wikipedia.org/wiki/Resource_starvation을 참고하라.

Page 3: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

또, 스레드의 작동을 모니터링 하는 것뿐만 아니라, 일정 시간이 소요된 이후나 어떤 예

외상황(exception)이 발생하였을 때 스레드를 중단하는 것도 단순한 일이 아니었다.

혹은, 여러 개의 스레드가 동일한 변수(필드)들을 공유할 경우, 그 변수에 대한 작업이 원

자적으로 처리되지 않을 경우 예상치 못한 실행 결과를 얻게 되기도 한다. 이 문제에 대

한 일반적인 해결책은 동기화 코드를 이용하는 것이지만, 산재되어 있는 동기화 코드와

비동기 코드를 최적화하는 것 역시 매우 고통스러운 일이었다. 동기화 코드를 이용할 경

우 병렬처리 능력을 최대한 이용할 수 없었고, 또, synchronized 블록을 이용하여 동기화

코드를 작성하는 것은 성능 저하의 큰 원인이 되기도 하였다.

THE CONCURRENCY PACKAGE:

이러한 상황에서, Java의 다섯 번째 버전(1.5, tiger)에 새로운 병렬처리 패키지

(concurrency package)가 추가되어 발표되었다. (후에 이 패키지는 Java SE6와 Java SE7에

서 더욱 더 개선되었다.)

먼저 이 패키지에는 원시자료형의 처리를 위한 원자적 래퍼 클래스들5이 포함되어 있다.

이 클래스들의 역할을 쉽게 이해하기 위해 x라는 integer 변수에 1을 더해주는 ‘x++’ 연

산에 대해 생각해보자. 이 코드는 눈에 보이는 것과는 달리 두 가지 작업을 수행한다. :

(1) x 값을 읽는 작업과 (2) x에 x+1을 쓰는 작업이다. 이런 종류의 작업은 멀티스레드 환

경에서 개발자의 의도와는 다른 동작을 할 가능성이 높은데, 그래서 잘못 동작하는 것을

방지하기 위해, ‘x’ 변수에 대해 작업을 수행하고 있는 스레드가 어떤 값을 저장하기 전까

지는 다른 스레드가 이 변수에 대해 읽거나 쓰는 작업을 수행하지 않고 대기해야 한다.6

이 문제를 해결하기 위해 Java SE57는 AtomicInteger나 AtomicFloat 등과 같은 원자적 래

퍼 클래스를 제안하였다. 이들 클래스에는 getAndIncrement, incrementAndGet,

getAndDecrement 등과 같은 원자적 처리를 보장해주는 메소드들이 포함되어 있다.

5 Atomic Primitive Wrapper classes. java.util.concurrent.atomic 패키지에 포함되어 있는 Atomic*

클래스들을 의미한다.

6 즉, 읽은 다음 수정하는 두 가지 작업을 원자화시켜야 한다는 것이다. 기존에는 이런 류의 작업

을 원자화하기 위해 synchronized 블록을 이용하였다.

7 원문에서는 Java SE7에서 제안된 것으로 언급되어 있으나 실제로는 Java SE5부터 제안된 클래스

이다.

Page 4: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

이뿐만이 아니라, Concurrent 패키지는 복잡다단한 스레드 제어 구조들을 추상화하여 손

쉽게 사용할 수 있게 하는 Executor 프레임워크 개념을 최초로 도입하였다. Executor들은

Runnable 인스턴스들을 포함할 수 있고, 그들의 라이프 사이클이나 스레드 풀을 관리할

수 있도록 설계되어 있다.

또, 이전까지만 하더라도, 어떤 스레드의 실행 결과를 그 스레드를 실행한 다른 스레드가

받는 것이 불가능하였다. 그래서, 과거에는 이 문제를 해결하기 위해 공유 객체(shared

object)를 이용하였는데, 이제는 Callable이라는 클래스를 이용하여 스레드의 실행 결과를

반환 받을 수 있게 되었다.

Callable은 현재의 스레드가 아닌 별개의 스레드에서 실행되도록 설계되었다는 점에서

Runnable과 비슷하다. 차이점이 있다면 Callable이 Future 객체 형태로 결과를 반환할 수

있다는 것인데, Future는 ‘미래에 만들어질’ 데이터를 의미한다. Future 객체를 이용하면

그것과 관련된 스레드의 실행이 종료되었는지, 아니면 예외가 던져졌는지를8 비동기적으

로 확인할 수 있으니 매우 유용한 구조라고 할 수 있다.

이제 Executor와 Callable을 이용한 간단한 예제를 살펴보도록 하자. 먼저 학생들과 학생

들이 수강한 세 가지 과목에 대한 점수 목록을 가지고 있다고 가정해 보자. 우리가 해야

할 일은 각 학생들의 평균 점수를 계산하고 출력하는 것이다.

스레드를 이용하지 않는다면, 각 학생 목록을 순회(iterate)하면서 하나씩 평균 점수를 계

산하고 출력하면 된다. 하지만 여기서는 당연히 병렬적으로 처리하도록 해 보자. 아래 프

로그램은 각각의 학생들마다 스레드를 하나씩 생성하여 각 학생들의 평균 점수를 반환하

는 코드이다.

1.

2.

3. import java.util.ArrayList;

4. import java.util.List;

5. import java.util.concurrent.Callable;

6. import java.util.concurrent.ExecutionException;

7. import java.util.concurrent.ExecutorService;

8. import java.util.concurrent.Executors;

9. import java.util.concurrent.Future;

10.

11. public class ExecutorInJava {

8 Exception이 ‘던져진다’라는 표현은 언제 봐도 기발한 표현이다.

Page 5: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

12. public static void main(String[] arg) {

13. // Prepare list of 'Callable' students

14. List<Student> students = new ArrayList<Student>();

15. students.add(new Student("Bob", 66, 80, 95));

16. students.add(new Student("Tom", 94, 82, 72));

17. students.add(new Student("Joy", 88, 85, 99));

18. students.add(new Student("Mills", 82, 75, 89));

19.

20. // Create Executor service with 3 threads in a pool

21. ExecutorService executor = Executors.newFixedThreadPool(3);

22. // Ask executor to invoke all of the operations

23. List<Future<Float>> results = null;

24. try {

25. results = executor.invokeAll(students);

26. } catch (InterruptedException e1) {

27. e1.printStackTrace();

28. }

29.

30. // Print the results

31. for (Future<Float> future : results) {

32. try {

33. System.out.println(future.get());

34. } catch (InterruptedException | ExecutionException e) {

35. e.printStackTrace();

36. }

37. }

38. }

39. }

40.

41. class Student implements Callable<Float> {

42. String name;

43. Integer subject1;

44. Integer subject2;

45. Integer subject3;

46.

47. public Student(String name, Integer subject1, Integer subject2,

48. Integer subject3) {

49. super();

50. this.name = name;

51. this.subject1 = subject1;

Page 6: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

52. this.subject2 = subject2;

53. this.subject3 = subject3;

54. }

55.

56. @Override

57. public Float call() throws Exception {

58. return (subject1 + subject2 + subject3) / 3.0f;

59. }

60. }

61.

이미 느꼈겠지만, Concurrent 패키지를 이용하면 병렬처리 프로그램을 이렇게 간단하게

만들 수 있다. 우리가 한 일이라고는 Callable 인터페이스를 구현한 Student 인스턴스의

목록을 ExecutorService의 invokeAll 메소드에 넘겨준 것 밖에는 없다. 우리가 고민하지

않아도 ExecutorService가 스레드풀 안에 생성되어 있는 세 개의 스레드에 Callable 작업

들을 할당하는 일을 알아서 해 준다.

invokeAll 메소드는 Future 객체 집합(Collection)을 반환한다. Future 객체를 이용하면 해

당 스레드 작업이 종료되었는지, 혹은 예외가 던져졌는지를 확인할 수 있으며, 언제라도

현재 진행되고 있는 동작을 취소할 수도 있다. 일반 스레드를 이용했던 예전에는 동작

취소 기능을 구현하는 것은 매우 번거로운 일이었다. invokeAll 메소드는 넌블럭킹(non-

blocking) 방식으로 동작하기 때문에, 언제라도 Future 집합을 순회하면서 병렬적으로 처

리된 결과를 이용할 수 있다.

Executor들은 동시 작업의 관리라는 측면에서 과거의 단순한 스레드와 비교한다면 엄청

난 발전이라고 볼 수 있다. Executor들이 기반하고 있는 알고리즘은 이른바 ‘분할 정복9’

알고리즘인데, ‘맵-앤드-리듀스 10 ’ 알고리즘이라고 말하기도 한다. 이 알고리즘의 핵심은,

아주 큰 작업을 작은 작업 뭉치(chunk)로 분할하고, 작은 작업들을 병렬적으로 처리하여

얻은 각각의 결과를 결합하여 최종적인 결과를 얻는 방식이다. 병렬로 동작할 하위 작업

들을 확인하고 그것들을 별개의 작업으로 나누는 것을 ‘매핑(mapping)’이라고 하고, 하위

작업들의 수행 결과를 결합하여 최종 결과물을 만들어내는 것을 ‘리듀싱(reducing)’ 이라

9 Divide and Conquer Algorithm

10 Map And Reduce Algorithm. 원래 MapReduce는 Google이 제안한 분산 처리 모델이다. 이 글

의 저자는 분할-정복(Divide and Conquer) 알고리즘을 맵-앤드-리듀스 알고리즘과 같은 의미로 사

용하고 있는 듯 하다.

Page 7: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

고 한다.

위의 문제를 약간 변경해보자. 전체 학급의 과목1의 총 평균을 구한다고 해 보자. 보통은

학생들의 목록을 순회하면서 과목1의 총합을 구한 뒤 학생의 수로 나누면 원하는 값을

얻을 수 있다.

하지만 ‘맵-앤드-리듀스’ 알고리즘을 이용하면 조금 다른 방법으로 이 작업을 수행할 수

있다. 학급 내 모든 학생의 점수 총합을 구하는 일은 매우 큰 작업이니, 이 작업을 여러

개의 작은 작업으로 나누어 보자. 먼저 전체 목록을 순회하면서 학생 다섯 명을 한 묶음

으로 하는 작업 뭉치를 만든다. 즉, 학생 다섯 명을 위해 하나의 Callable 객체를 만들어

그것에 5명을 할당한다는 것이다.

결국 100명의 학생이 있는 학급이라면 20개의 스레드가 각각 자신의 작업 뭉치(다섯 명

의 학생)에 대해서만 평균을 구하는 작업을 수행하면 된다. 그 뒤에 얻어진 Future 집합

을 순회하여 각각의 평균값을 더한 뒤 작업 뭉치의 수(여기서는 20)로 나누기만 하면 된

다. 맵-앤드-리듀스 알고리즘은 확실히 단일 스레드 모델에 비해 엄청난 속도를 보인다.

Executor에도 문제점은 있는데 바로 병렬 처리와 관련된 부분이다. Callable 객체 A가 다

른 Callable 객체 B의 처리 결과를 기다려야 한다면, A는 대기(waiting) 상태에 빠지게 되

어 다른 Callable 작업을 대체하여 수행할 수 있는 기회를 잃어버리게 된다.11 이 문제를

해결하기 위해 Java SE7에서 Fork and Join 프레임워크가 제안된 것이다. 이제 Fork and

Join을 상세히 알아보도록 하자.

FORK AND JOIN:

Java SE7에 새롭게 추가된 ForkJoinPool executor는 ForkJoinTask 인터페이스를 구현한 클

래스 인스턴스를 실행하기 위해 제안되었다. 이 executor는 하위 작업들을 생성하고 그

하위 작업들이 완료될 때까지 대기할 수 있는 기능을 제공한다. ForkJoinPool executor의

가장 큰 특징은, 어떤 작업이 다른 작업이 종료되기를 기다리고 있고, 처리해야 하는 다

른 작업이 대기하고 있을 때, 스레드 풀 간에 ‘작업을 훔쳐12’오는 방법으로 해당 작업을

처리할 수 있다는 점이다. 이런 알고리즘을 워크 스틸링 알고리즘이라고 한다.

11 결국 병렬처리를 한다 하더라도 다른 thread의 처리 결과를 기다려야 한다면 그곳에서

blocking이 발생하고 그로 인해 해당 스레드는 다른 작업을 수행할 수 없게 된다는 의미이다.

12 By stealing jobs

Page 8: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

이상적으로는, 이 알고리즘을 사용하면 스레드풀 내의 어떠한 스레드도 유휴상태(idle)로

남아있지 않게 된다. 즉, 유휴상태의 스레드는 현재 작업중인 스레드로부터 작업을 훔쳐

온 뒤 작업을 시작할 것이다.

ForkJoinPool은 ExecutorService의 구현체들 중 특화된 클래스로, 워크 스틸링 알고리즘을

구현한 것이다. 이 클래스는 ForkJoinTask 내에 작성된 코드를 실행할 수 있게 되어 있다.

ForkJoinTask를 구현한 클래스로는 13 RecursiveTask와 RecursiveAction가 있는데,

RecursiveAction이 실행 결과를 반환할 수 없다는 점을 제외하면 이 두 가지는 거의 동일

하다고 할 수 있다.

ForkJoinTask 클래스에는 두 가지 메소드가 정의되어 있는데 하나는 ‘fork’이고 다른 하나

는 ‘join’이다.

fork메소드는 ForkJoinTask에 대한 실행 계획을 수립하고 이미 존재하는 ForkJoinTask로부

터 새로운 ForkJoinTask를 생성할 수 있도록 한다.

join메소드는 하나의 ForkJoinTask가 다른 ForkJoinTask의 작업이 완료될 때까지 대기할

수 있도록 한다.

이제 Fork and Join 프레임워크를 이용하여 실제로 동작하는 프로그램을 작성해 볼 것이

다. 이를 위해 여기서는 그 유명한 피보나치 수열14을 이용하기로 하였다.

Index 0 1 2 3 4 5

Element 0 1 1 2 3 5

위 표는 피보나치 수열에서 최초 여섯 개의 수15이다. 일단 이해를 돕기 위해, 최초 25개

의 피보나치 수를 생성하고 출력하는 단일 스레드 프로그램을 작성해 보도록 하자.

13 ForkJoinTask는 인터페이스가 아니라 추상(Abstract) 클래스이다.

http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinTask.html를 참고하라

14 Fibonacci Series. 피보나치라는 별명을 갖고 있던 Leonardo of Pisa라는 이탈리아 수학자에 의

해 제안된 수열이다. 피보나치 수열은 다음 조건을 만족한다. Fn = Fn-1 + Fn-2. 단, F1 = 1, F2 = 1, n

> 2. 피보나치 수에 대해서는 http://en.wikipedia.org/wiki/Fibonacci_number를 참고하라.

15 원래 피보나치 수열은 n=1부터 시작한다.

Page 9: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

1.

2. import java.util.ArrayList;

3. import java.util.List;

4.

5. public class FibonnacciSeries {

6. public static void main(String[] arg) {

7. int size = 25;

8. List<integer> fibinacciSeries = new ArrayList<>();

9. for (int index = 0; index < size; index++) {

10. fibinacciSeries.add(FibonnacciGenerator.generate(index));

11. }

12. dumpList(fibinacciSeries);

13. }

14.

15. public static void dumpList(List list) {

16. int index = 0;

17. for (Object object : list) {

18. System.out.printf("%d\t%d\n", index++, object);

19. }

20. }

21. }

22.

23. class FibonnacciGenerator {

24. public static Integer generate(Integer index) {

25. if (index == 0) {

26. return 0;

27. }

28. if (index < 2) {

29. return 1;

30. }

31. Integer result = generate(index - 1) + generate(index - 2);

32. return result;

33. }

34. }

이 프로그램을 실행하면 아래와 같은 결과를 얻을 수 있다

0 0

1 1

Page 10: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

2 1

3 2

4 3

5 5

6 8

7 13

8 21

9 34

10 55

11 89

12 144

13 233

14 377

15 610

16 987

17 1597

18 2584

19 4181

20 6765

21 10946

22 17711

23 28657

24 46368

이 프로그램은 메인 스레드 말고는 스레드를 전혀 사용하지 않는다. 그리고 원하는 값을

얻기 위해 각 수열의 수 마다 여러 번의 반복(iteration)을 해야 한다. 얻고자 하는 피보나

치 수가 커지면 커질수록 성능이 급격히 떨어지는 것을 확인할 수 있을 것이다.16 이제

동일한 문제를 해결하기 위해 Fork and Join 프레임워크를 이용해보기로 하자.

1.

2. import java.util.ArrayList;

3. import java.util.Calendar;

4. import java.util.List;

5. import java.util.concurrent.ForkJoinPool;

16 이 프로그램은 전형적인 재귀호출 방식을 이용하고 있다. 다음의 두 가지 방식을 이용하면 단

일 스레드를 이용하더라도 이보다 훨씬 빠르게 동작하는 프로그램을 작성할 수 있다. (1) 단일 for

문 방식. (2) cache 방식

Page 11: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

6. import java.util.concurrent.RecursiveTask;

7.

8. public class FibonacciSeries_ForkJoin {

9. public static void main(String[] arg) {

10. int size = 25;

11. Long startTime = Calendar.getInstance().getTimeInMillis();

12. final ForkJoinPool pool = new ForkJoinPool();

13. List fibonacciSeries = new ArrayList<>();

14. for (int index = 0; index < size; index++) {

15. FibonacciSeriesGeneratorTask task = new FibonacciSeriesGeneratorTask(

16. index);

17. fibonacciSeries.add(pool.invoke(task));

18. }

19. Long endTime = Calendar.getInstance().getTimeInMillis();

20. System.out.println(endTime - startTime);

21. dumpList(fibonacciSeries);

22. }

23.

24. public static void dumpList(List list) {

25. int index = 0;

26. for (Object object : list) {

27. System.out.printf("%d\t%d\n", index++, object);

28. }

29. }

30. }

31.

32. class FibonacciSeriesGeneratorTask extends RecursiveTask {

33. private static final long serialVersionUID = 1L;

34. private Integer index = 0;

35.

36. public FibonacciSeriesGeneratorTask(Integer index) {

37. super();

38. this.index = index;

39. }

40.

41. @Override

42. protected Integer compute() {

Page 12: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

43. if (index == 0) {

44. return 0;

45. }

46. if (index < 2) {

47. return 1;

48. }

49. final FibonacciSeriesGeneratorTask worker1 = new

FibonacciSeriesGeneratorTask(index - 1);

50. worker1.fork();

51.

52. final FibonacciSeriesGeneratorTask worker2 = new

FibonacciSeriesGeneratorTask(index - 2);

53. return worker2.compute() + worker1.join();

54. }

55. }

놀랄 것도 없이, 이 코드의 실행 결과는 이전의 단일 스레드 프로그램의 결과와 완벽하

게 똑같다. 차이점이 있다면 여러 개의 작업 스레드를 이용하여 실행 속도를 단축했다는

것이다.

위 코드에서는 ForkJoinPool의 기본 생성자를 이용하여 인스턴스를 생성하였다. 많은 개

발자들이 습관적으로 다음과 같이 가용 프로세서 개수를 생성자에 전달하는 방식으로

ForkJoinPool을 생성한다.

New ForkJoinPool (Runtime.availableProcessors());

하지만 굳이 이럴 필요가 없는데, 그 이유는 ForkJoinPool의 기본 생성자가 알아서 가용

프로세서당 병렬처리를 적용하기 때문이다.

이제 각 피보나치 수를 구하기 위해 FibonacciSeriesGeneratorTask 인스턴스를 생성하고

스레드풀의 invoke 메소드에 전달하기만 하면 된다.

FibonacciSeriesGeneratorTask는 RecursiveTask17 18를 구현하였다. RecursiveAction을 구현

17 RecursiveTask 역시 인터페이스가 아니라 추상 클래스이다.

18 RecursiveTask의 javadoc을 보면 이 예제와 동일한 형식의 코드를 확인할 수 있다.

http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/RecursiveAction.html를 확인하라.

Page 13: Introduction to Fork Join Framework - ECLIANeclian.sys4u.co.kr/wp-content/uploads/2013/02/... · 성능을 최대한 이용할 수 있도록 병렬 프로그래밍, 혹은 멀티코어

하지 않은 이유는 위에서 말했듯 RecursiveAction이 처리 결과를 반환할 수 없기 때문이

다. 피보나치 수의 계산 결과가 반환되어야 했기 때문에 RecursiveTask를 이용하였다.

FibonacciSeriesGeneratorTask는 compute 메소드를 구현하고 있는데, 이 메소드는 또 다

른 FibonacciSeriesGeneratorTask를 생성한 뒤 fork하고 있다. 그리고 join 메소드는 fork

된 스레드가 결과를 반환할 때까지 현재 스레드가 대기하도록 할 것이다.

CONCLUSION19

지금까지 본 대로, Fork Join 프레임워크는 스레드 가용성을 극대화하여 어플리케이션의 성능을

향상시킴과 동시에, 기존에는 구현하기 힘들었던 재귀적인 작업을 수행하는 병렬 프로그램을 쉽

고 빠르게 작성할 수 있도록 도와준다.

하지만 Fork Join 프레임워크를 이용하건 Executor 프레임워크를 이용하건, 스레드의 기본 개념인

동시작업, 공유자원, 배타성 등의 원칙을 제대로 이해하지 못한다면 제대로 동작하는 코드를 작성

할 수 없다. Java 병렬처리에 대한 기본적이면서도 핵심적인 내용을 확인하고 싶다면 [Java

Concurrency in Practice]를20 읽어보기를 권한다.

19 원문에는 결론이 생략(?)되어 있다. 이 부분은 전적으로 역자 마음대로 적은 것이다.

20 이 책은, Java에서 지원하는 병렬처리 기법뿐만 아니라, 병렬 프로세싱에 대한 기본적인 내용을

상세히 다루고 있다. 병렬처리에 관심이 많은 사람이라면 꼭 읽어보기를 추천한다.

저자 : Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea 공저.

출판사 : Addison Wesley Professional

출판일 : 2006년 5월 9일