멀티스레드를 사용하는 이유는 시스템의 성능을 극대화하고 지연 시간을 최소화 하여 더욱 효율적인 처리를 가능하게 하기 위해서다.
성능이란 부문은 소프트웨어 측면에선 분야에 따라 다르게 해석된다.
- 고속 거래 시스템의 성능 기준 : 지연 시간
- 영상 플레이어의 성능 기준 : 프레임 레이트의 정확도
- 머신러닝의 성능 기준 : 처리량
멀티스레딩에선 보통 성능을 처리량으로 생각한다.
처리량이란 일정 시간동안 완료한 작업의 양으로 시간 단위당 작업으로 측정된다.
지연 시간과 멀티스레딩
지연 시간이란 시간 단위로 측정되고, 작업 하나의 완료 시간으로 정의된다.
싱글스레드를 사용하는 경우 일반적으로 지연시간은 (모든 작업 수행 시간 합 / 작업 개수) 이다.
그렇다면 이 지연시간을 줄이기 위해선 어떻게 해야할까? (모든 작업 수행 시간 합 / 작업 개수) 여기서 나온 수를 어떻게 줄일까?
정답은 분모의 수(N)를 키우는 것 이다.
분모의 수를 키우는 방법은 CPU의 코어를 이용하는 것이다.
싱글코어는 병렬 작업이 어렵다. (물론 하이퍼스레딩을 사용하면 코어 하나로 스레드 두 개를 동시에 실행할 수 있음)
하지만 멀티코어의 CPU에선 여러 작업을 병렬로 스케줄링하여 수행하므로, 이를 통해 지연 시간을 단축시킬 수 있다.
하지만 멀티코어 환경에서 멀티스레딩을 사용한다고 무조건 성능이나 지연시간에서 이득을 볼 수 있는 것은 아니다.
하나의 작업을 여러 개 하위 작업으로 나누는 비용 + 하위 작업들을 수행하기 위한 스레드 생성 및 전달 비용 + OS가 스레드를 스케줄링하여 작업을 실제로 할당하는 비용 + 작업 결과를 합치는 비용 + ・・・
멀티스레딩에서도 위 처럼 많은 비용이 소모된다.
그렇기 때문에 개발자는 싱글스레드를 사용해도 되는 작업인지, 아니면 멀티스레드를 사용해야 더 이득을 볼 수 있는 작업인지 판단할 줄 알아야 한다.
만약 블로킹 호출(ex) DB 연결)이 없고 단순한 계산만 하는 문제에선 멀티스레드 방식이 역효과를 낳을 수 있다.
스레드 풀링
스레드 풀링이란, 위에서 언급한 성능과 지연시간을 최적화하기 위한 기술이다.
스레드 풀이란 곳에 개발자가 설정한 갯수만큼 스레드를 생성한 뒤, 담아두고, 재사용하는 방식이다. 이 방식을 사용하면 매번 스레드를 생성하는데 드는 비용을 절감할 수 있다.
실습 코드
public class ReadTxtHttpServer {
static final int NUMBER_OF_THREAD = 1;
static final String FILE_PATH = "resources/war_and_peace.txt";
static String text = "";
public static void main(String[] args) throws IOException {
text = new String(Files.readAllBytes(Paths.get(FILE_PATH)));
startServer();
}
public static void startServer() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
server.createContext("/search", new WordCountHandler());
ExecutorService executor = Executors.newFixedThreadPool(NUMBER_OF_THREAD);
server.setExecutor(executor);
server.start();
}
private static class WordCountHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
String query = exchange.getRequestURI().getQuery();
String[] keyValue = query.split("=");
String action = keyValue[0];
String word = keyValue[1];
if (!action.equals("word")) {
exchange.sendResponseHeaders(400, 0);
return;
}
long count = countWords(word);
byte[] response = Long.toString(count).getBytes();
exchange.sendResponseHeaders(200, response.length);
OutputStream responseBody = exchange.getResponseBody();
responseBody.write(response);
responseBody.close();
}
private long countWords(String word) {
long count = 0;
int idx = 0;
while (idx >= 0) {
idx = text.indexOf(word, idx);
if (idx >= 0) {
count++;
idx++;
}
}
return count;
}
}
}
위의 코드는 war_and_peace.txt 파일에 요청의 쿼리 파라미터에 온 단어가 몇 개 들어있는지 계산하는 코드이다.
스레드 1개
위의 코드에서 NUMBER_OF_THREAD 를 1로 설정한 뒤, JMeter를 사용해 테스트한 결과, Throughput(처리량)이 301.4 가 나왔다. (1초당 약 301개의 요청을 처리)
스레드 7개
다음은 내 CPU 성능에 맞게 NUMBER_OF_THREAD을 7개로 재설정하고 테스트한 결과, Throughput(처리량)이 875.7 가 나왔다. (1초당 약 875개의 요청을 처리)
위의 결과를 보다시피 스레드 풀을 사양에 맞게 적절히 사용한다면 처리량을 2배 이상 높일 수 있다.
참조
https://www.udemy.com/course/java-multi-threading/?couponCode=24T6MT102824
'백엔드 > Java' 카테고리의 다른 글
[Java] 멀티스레딩 - 가상 스레드 (0) | 2024.11.04 |
---|---|
[Java] 멀티스레딩 - 스레드 간 통신 (2) | 2024.11.04 |
[Java] 멀티스레딩 - 락 심화 (0) | 2024.11.04 |
[Java] 멀티스레딩 - 스레드간 데이터 공유 (0) | 2024.10.31 |
[Java] 멀티스레딩 - 스레드 조정 (0) | 2024.10.25 |