Android에서의 스레드는 크게 2가지로 나뉜다.
UI를 업데이트 할 수 있기에 UI 스레드라고 이름 붙여진 메인 스레드.
UI 업데이트 이외에 네트워크 작업 등 무거운 작업을 하게 되는 워크 스레드.
메인 스레드는 하나의 어플에서 단 하나만 존재하고 워크 스레드는 n개 이상 존재한다.
ANR 파트에서 다뤘지만 UI스레드에서 너무 많은 작업을 하는걸 안드로이드는 지양한다.
📍 그말은 즉, 워크 스레드를 적절히 이용해서 Main Thread에 무거운 작업을 시키지 않고 뷰에 업데이트를 잘 시키는게 또 하나의 숙제이다.
스레드를 잘 관리하는 게 그만큼 중요한데 이전에 동시성 문제라던가 여러 문제를 스레드로부터 지켜내기 위해 다뤘었다.
오늘은 스레드를 병렬 처리 하는 방법에 대해 이야기 하고자 한다.
🖐 모두 java 라이브러리의 concurrent package에 생성되어 있다.
🔍 근데 그럼 concurrent를 뭐라고 생각하면 될까?
사전의미 그대로 라면 동시적으로, 동시성 을 의미한다.
concurrent는 parrallel과 보통 같이 이야기가 된다.
동시성과 병렬에 대한 이야기를 여기서 하면 너무 길어질 것이다.
아주 간단하게만 이야기 해보자.
그래서 일반적으로 Concurrent Programming을 많이 한다.
CPU가 좋아지는 요즘 시대에는 Parallel Programming은 선택이 아니라 필수가 되어가고 있다.
Concurrent 단어를 보고 잠깐 병렬/동시성 프로그래밍을 이야기 했다.
다시 Executors를 보자.
"Java에선 java.util.concurrent.Executors와 java.util.concurrent.ExecutorService를 제공하며 이를 이용하면 간단하게 스레드 풀을 생성하여 병렬 처리할 수 있다."
"Executors 클래스에서의 여러 static 메서드를 사용하여 ExecutorService 인터페이스의 구현 객체를 만들 수 있는데, 이러한 객체가 스레드 풀이다."
📝 ExecutorService 인터페이스의 구현 객체 = 스레드 풀
위의 그림과 같이 Executor Service는 스레드 풀과 Queue로 구성되어 있다.
각각의 task들은 큐에 들어가게 되고 순차적으로 스레드에 할당된다.
스레드가 없다면 큐에서 대기하게 된다.
스레드를 생성하는 것은 비용이 큰 작업이기에 이를 최소화 하기 위해 미리 스레드 풀 안에 스레드를 생성해놓고 관리한다.
fun main() {
val executor = Executors.newFixedThreadPool(2)
(0..9).forEach { _ ->
executor.execute {
try {
TimeUnit.MILLISECONDS.sleep(1000)
val threadName = Thread.currentThread().name println("hello $threadName")
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
executor.shutdown()
if(executor.awaitTermination(10, TimeUnit.SECONDS)) {
println("작업 종료")
} else {
println("작업 종료 X")
executor.shutdownNow()
}
}
우선, Callable
Runnable과 비슷하지만 리턴 타입을 갖는다는 특징이 있다.
Callable<Integer> task = () -> {
try {
TimeUnit.SECONDS.sleep(1);
return 123;
}
catch (InterruptedException e) {
throw new IllegalStateException("task interrupted", e);
}
};
출처: https://emong.tistory.com/221
1초 이후에 123을 리턴하는 코드 이다.
그렇다면 Executor service에서 Callable은 어떻게 리턴 값을 받는가?
해답은 아래의 코드에 있다.
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);
출처: https://emong.tistory.com/221
submit()을 쓰면 리턴 값을 받을 수 있다고 했다.
🖐 어 근데?? Future에 받네?? 그래 이제 Future가 등장한다.
Future는 executor service의 리턴값을 특정 시간 이후에 돌려 받을 수 있다. 리턴값 가져 오는 애라고 생각하면 된다.
근데 그럼 첫 번째 System.out.println()에서 true/false 중 뭘 출력할까??
정답은 false이다. 😛
future.get()
를 써야 비로소 리턴 값을 기다리는 future가 되는 것이다.
리턴 받을 때까지 코드의 실행은 멈추게 된다.👐
그럼 아래 System.out.println을 통해 true와 123을 리턴한다.
물론 Executor를 더 알아보기 위해선 task, thread pool 등을 더 자세하게 다뤄야 할 것이다.
이 한 페이지에 담기엔 너무 많아질 것 같아서 오늘은 간단하게 Executor를 알아본 것이다.
스레드 풀에 대해 좀 더 알고 싶다면 이 블로그를 보면 좋다.