ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 쓰레드 풀(Thread pool)을 이용해 이미지 크롤러 성능 높이기
    Java 2022. 8. 5. 22:49

    Thread pool

    쓰레드를 잘 만져서 웹 크롤러의 성능을 높여보도록 한다.

     

    일단 Jsoup을 이용해서 셔터스톡에서 1,000장의 이미지를 스토리지에 저장하는 프로그램을 만들었다.

     

    성능의 측정 기준은 프로그램이 완료하는데 걸리는 시간으로 했다.

     

    https://github.com/ghchoi0427/SocketServer/blob/master/src/main/java/threadpool

     

    GitHub - ghchoi0427/SocketServer: 멀티쓰레딩, pub/sub 패턴, thread pool

    멀티쓰레딩, pub/sub 패턴, thread pool. Contribute to ghchoi0427/SocketServer development by creating an account on GitHub.

    github.com

     

    이번 글에서 다룰 방법들은:

    [Thread pool 이용 X]

    1. 싱글쓰레드

    2. 멀티쓰레드

    [Thread pool 이용 O]

    3. NewCachedThreadPool

    4. NewFixedThreadPool

    이다.

     

     

    1. 싱글 쓰레딩

    package threadpool;
    
    import threadpool.crawlanddownload.Crawler;
    import threadpool.crawlanddownload.Downloader;
    
    import java.util.List;
    
    public class SingleThread implements DownloadImage{
    
        private final String baseUrl = "https://www.shutterstock.com/search/";
    
        @Override
        public void run(String keyword, int page) {
            Crawler crawler = new Crawler();
            Downloader downloader = new Downloader("C:\\temp");
            List<String> imageSourceList = crawler.getImageSourceList(baseUrl + keyword, page);
            imageSourceList.forEach(downloader::downloadImage);
        }
    }

    가장 기본적인 웹 크롤링 프로그램이다. 단일 쓰레드로 작동하며,

    싱글 쓰레드 결과

    1분 10초만에 1,000장의 이미지 다운로드를 완료했다.

     

    2. 멀티쓰레딩

    package threadpool;
    
    import threadpool.crawlanddownload.Crawler;
    import threadpool.crawlanddownload.Downloader;
    
    import java.util.List;
    
    public class MultiThread implements DownloadImage {
        private final String baseUrl = "https://www.shutterstock.com/search/";
    
        @Override
        public void run(String keyword, int page) {
            Crawler crawler = new Crawler();
            Downloader downloader = new Downloader("C:\\temp");
            List<String> imageSourceList = crawler.getImageSourceList(baseUrl + keyword, page);
    
            imageSourceList.forEach(src->{
                Runnable runnable = () -> downloader.downloadImage(src);
                new Thread(runnable).start();
            });
        }
    }

    imageSourceList에는 이미지 url이  String형식으로 저장되어 있다.

    이 코드에서는 리스트에 저장된 url마다 하나의 쓰레드를 생성해서 다운로드를 완료한다.

    1,000개의 쓰레드가 생성되며, 실행 후 소멸된다.

     

    실행 결과 17초가 소요되었다. 

     

    3. CachedThreadPool

    package threadpool;
    
    import threadpool.crawlanddownload.Crawler;
    import threadpool.crawlanddownload.Downloader;
    
    import java.util.List;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class CachedThreadPool implements DownloadImage {
        private final String baseUrl = "https://www.shutterstock.com/search/";
    
        @Override
        public void run(String keyword, int page) {
            ExecutorService service = Executors.newCachedThreadPool();
            Crawler crawler = new Crawler();
            Downloader downloader = new Downloader("C:\\temp");
            List<String> imageSourceList = crawler.getImageSourceList(baseUrl + keyword, page);
            imageSourceList.forEach(src -> service.submit(() -> downloader.downloadImage(src)));
            service.shutdown();
        }
    }

    마찬가지로 17초 소요되었다.

    이번에는 FixedThreadPool과의 비교를 위해 현재 실행중인 쓰레드를 확인하는 코드를 하나 삽입해본다.

    imageSourceList.forEach(src -> service.submit(() -> {
        downloader.downloadImage(src);
        System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName()); //여기
    }));

    CachedThreadPool 실행 쓰레드 확인

    각각 다른 1,000개의 쓰레드가 생성되었다. CachedThreadPool 방식은 필요한 만큼 쓰레드를 생성함을 알 수 있다.

     

    4. FixedThreadPool

    package threadpool;
    
    import threadpool.crawlanddownload.Crawler;
    import threadpool.crawlanddownload.Downloader;
    
    import java.util.List;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class FixedThreadPool implements DownloadImage {
    
        private final int nThread;
        private final String baseUrl = "https://www.shutterstock.com/search/";
    
        public FixedThreadPool(int nThread) {
            this.nThread = nThread;
        }
    
        @Override
        public void run(String keyword, int page) {
            ExecutorService service = Executors.newFixedThreadPool(nThread);
            Crawler crawler = new Crawler();
            Downloader downloader = new Downloader("C:\\temp");
            List<String> imageSourceList = crawler.getImageSourceList(baseUrl + keyword, page);
    
            imageSourceList.forEach(src -> {
                Runnable runnable = () -> downloader.downloadImage(src);
                service.submit(runnable);
            });
            service.shutdown();
        }
    }

    FixedThreadPool 방식은 다른 세 방식과 다르게 nThread를 인수로 받았다. 고정된 쓰레드 수이다. 

     

    쓰레드 수 소요 시간
    8 22s
    16 14s
    32 17s
    64 21s
    100 16s
    128 19s
    256 20s
    512 18s
    1024 22s
    2048 18s
    4096 17s
    8192 27s

    고정된 쓰레드의 개수에 변화를 줘가며 실행시간을 측정해봤다.

    이를 토대로 곡선 그래프를 도출해보고 싶었지만 아쉽게도 쓰레드 수에 따른 유의미한 결과를 얻을 수 없었다.

    실행 할 때마다 소요시간이 크게 차이 나기도 하고 쓰레드 수를 필요 이상으로 만들었음에도 성능 저하가 보이지 않았다.

    컴퓨터의 여러 프로그램으로 인한 하드웨어 상태, 네트워크 상태 등이 영향을 끼친 것 같다.

     

    일단 쓰레드 5개로 고정시키고 실행중인 쓰레드를 출력해보면 

    이렇게 1~5 번 쓰레드가 반복해서 일을 처리한다.

     

     

    내가 원했던 그림

    암달의 법칙에 의하면 병렬처리 비율을 아무리 높인다 해도 처리속도가 선형적으로 빨라지진 않고 plateau 된다.

    분명히 코드 어딘가에 병목이 있을 것이다.

     

    이번 실험에서 유의미한 결과를 내진 못했지만 오늘 하루동안 수만장의 사진을 다운받으며 느낀 체감으로는 고정쓰레드풀에서 적절한 값을 넣어주는게 가장 성능이 좋았다. 

     

    쓰레드가 많으면 context switch가 많이 일어난다. 그래서 너무 많은 쓰레드를 생성하면 역효과가 발생할 수 있다.

    이러한 context switch를 줄여주기 위해서 Thread pool을 이용한다.

     

    ExecutorService는 이러한 쓰레드 작업을 효율적으로 하는걸 돕는 라이브러리다.

     

    service.submit(runnable);

    위의 쓰레드 풀을 사용하는 코드에서 submit을 해줬다.

    여기서 runnable(callable도 가능)이 task인데 이것들이 Task Queue에 쌓인다.

    그러면 Thread pool에 있는 쓰레드 들이 이것을 가져다가 처리해준다.

     

    task보다 thread가 많으면 thread가 각각 하나씩 맡아서 처리할 수 있겠지만

    task가 thread보다 많으면? thread pool의 thread 중 일을 끝낸 친구가 task queue에서 기다리고 있는 task를 가져다 처리한다.

     

    결국 처리해야 할 작업 종류 등을 잘 고려해서 알맞은 방식을 고르는게 좋다!

    'Java' 카테고리의 다른 글

    Publish/Subscribe 패턴  (0) 2022.06.18
    [Java] Enum의 개념과 사용예제  (0) 2021.03.05
    JAVA8 Stream - 스트림 중개 연산  (0) 2021.03.05
    JAVA8 Stream - 스트림 생성  (0) 2021.02.26
Designed by Tistory.