자바/프로세스와 쓰레드

fork & join 프레임웍

백_곰 2022. 5. 10. 13:07

1. fork & join 프레임웍

- 이 프레임웍은 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하는 것을 쉽게

만들어 준다.

 

- 수행할 적업에 따라 두 클래스로 나눠지는데, 상속받아 구현해야 한다.

클래스 설명
RecursiveAction 반환값이 없는 작업을 구현할 때 사용
RecursiveTask 반환값이 있는 작업을 구현할 때 사용

 

public abstract class RecursiveAction extends ForkJoinTask<void>{
     ...
     protected abstract void compute(); // 상속을 통해 이 메서드를 구현해야 한다.
     ...
}

public abstract class RecursiveTask<V> extends ForkJoinTask<V>{
     ...
     V result
     protecteed abstract V compute(); // 상속을 통해 이 메서드를 구현해야 한다.
     ...
}

 

 

- fork()는 해당 작업을 쓰레드의 작업 큐에 넣는 작업을 한다. 

( 비동기 메서드)

 

- join()은 해당 작업의 수행이 끝날 때까지 기다렸다가, 수행이 끝나면 그 결과를 반환한다.

( 동기 메서드 )

 

- invoke()는 쓰레드 풀과 수행할 작업이 생성되면 작업을 시작하는 메서드이다.

 

- 쓰레드의 start()와 run()이 있다면, fork&join 프레임웍 invoke() compute()가 있는 것이다.

 

 

 

 

1-1. fork & join 프레임웍을 이해하기 위한 예제(1)

: from에서 to까지 모두 더하는 예제를 쓰레드 풀을 만들어 작업을 나눈 예제이다.

 

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class Exercise021 {
	public static void main(String[] args) throws Exception {
		ForkJoinPool pool = new ForkJoinPool();
		//쓰레드 풀 생성
		
		SumTask task = new SumTask(1, 10);
		//수행할 작업을 생성
		
		Long result = pool.invoke(task);
		//invoke()를 호출해서 작업을 시작
		
		System.out.println(result);
		int sum = 0;
	}
}

class SumTask extends RecursiveTask<Long>{
	long from, to;
	
	public SumTask(long from, long to) {
		this.from = from;
		this.to = to;
	}
	
	public long sum() {
		int sum = 0;
		
		for(long i = from; i<=to; i++) {
			sum += i;
		}
		return sum;
	}
	
	@Override
	protected Long compute() {
		long size = to - from +1;
		
		if(size <= 5) {
			return sum();
		}
		long half = (from+to)/2;
		
		SumTask leftSum = new SumTask(from, half);
		SumTask rightSum = new SumTask(half+1, to);
		
		leftSum.fork();
		// 작업 (leftSum)을 작업 큐에 넣는다.
		return rightSum.compute() + leftSum.join();
		}
}

 

( 나눠진 작업은 각 쓰레드가 골고루 나눠서 처리한다. )

 

( fork()는 비동기 메서드이므로, 호출하면 결과를 기다리지 않고 바로 다음 구문이 실행된다. )

( 그러다가 작업을 더이상 나눌 수 없게 되었을 때, compute()의 재귀호출이 끝나고 join()의 결과를 기다렸다가

더해서 결과를 반환한다. )

 

 

 

 

1-1. fork & join 프레임웍을 이해하기 위한 예제(1)

: 일반 for문과 fork&join 프레임웍의 성능을 비교하는 예제이다.

 

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class Exercise022 {
	static final ForkJoinPool pool = new ForkJoinPool();  // 쓰레드풀을 생성

	public static void main(String[] args) {
		long from = 1L;
		long to   = 100_000_000L;

		SumTask2 task = new SumTask2(from, to);

		long start = System.currentTimeMillis(); // 시작시간 초기화
		Long result = pool.invoke(task);

		System.out.println("Elapsed time(4 Core):"+(System.currentTimeMillis()-start));
		System.out.printf("sum of %d~%d=%d%n", from, to, result);
		System.out.println();

		result = 0L;
		start = System.currentTimeMillis(); // 시작시간 초기화
		for(long i=from;i<=to;i++)
			result += i;

		System.out.println("Elapsed time(1 Core):"+(System.currentTimeMillis()-start));
		System.out.printf("sum of %d~%d=%d%n", from, to, result);
	} // main의 끝
}

class SumTask2 extends RecursiveTask<Long> {
	long from;
	long to;

	SumTask2(long from, long to) {
		this.from = from;
		this.to    = to;
	}

	public Long compute() {
		long size = to - from;

		if(size <= 5)     // 더할 숫자가 5개 이하면
			return sum(); // 숫자의 합을 반환

		long half = (from+to)/2;

		// 범위를 반으로 나눠서 두 개의 작업을 생성
		SumTask2 leftSum  = new SumTask2(from, half);
		SumTask2 rightSum = new SumTask2(half+1, to);

		leftSum.fork();

		return rightSum.compute() + leftSum.join();
	}

	long sum() { // from~to의 모든 숫자를 더한 결과를 반환
		long tmp = 0L; 

		for(long i=from;i<=to;i++)
			tmp += i;

		return tmp;
	}
}

 

( for문보다 fork()&join 프레임웍으로 계산한 것이 더 빠르게 측정되었다. )

( 책에서는 옛날이다보니 for문이 더 빠르게 나왔지만, 현재 멀티 쓰레드로 처리하는 것이 더 빠르다. )

 

( 그러므로, 상황에 맞게 더 좋은 성능을 택하는 것이 좋다. )

 

 

 

 

2. 다른 쓰레드의 작업 훔쳐오기

- fork()가 호출되어 작업 큐에 추가된 작업 역시, compute()에 의해 더 이상 나눠지지 않을 때까지 반복해서 

나뉘고, 자신의 작업 큐가 비어있는 쓰레드는 다른 쓰레드의 작업 큐에서 작업을 가져와서 수행한다.

이러한 과정을 '작업 훔쳐오기' 라고 하며, 모두 쓰레드풀에 의해 자동적으로 이루어진다.