안드로이드 스레드 풀 사용

woga·2022년 3월 19일
0

Android 공부

목록 보기
21/49
post-thumbnail

스레드를 만들려면 Thread를 상속하거나 Thread(Runnable) 생성자에 Runnable을 넘기는 방법이 있지만, 스레드 풀을 사용하는 방법도 있다.

스레드 풀이란?

스레드 풀은 대기 상태의 스레드를 유지해서 스레드 종료/생성 오버헤드를 줄입으로써, 많은 개수의 비동기 작업을 실행할 때 퍼포먼스를 향상시킨다. 게다가 스레드 풀은 스레드를 포함한 리소스를 제한하고 관리하는 방법도 제공한다. 백그라운드에서 할 작업이 많다면 스레드 풀 사용을 고려해보자

Android : ThreadPoolExecutor

자바에서는 스레드 풀이 ThreadPoolExecutor 클래스로 구현되어 있다. 이제는 Deprecated된 AsyncTask도 내부적으로 ThreadPoolExecutor를 사용하고 있다.

어떤 앱에서 콜렉션에 Thread를 담아 직접 스레드 풀을 만든 것을 본 적이 있는데, 결국 메모리 누수가 발견됐다. 다시 발명하지 말고 잘 만들어진 ThreadPoolExecutor를 사용하자!

ThreadPoolExcutor에는 4개의 생성자가 있다. 그 중에서 ThreadFactory 파라미터를 뺀 세 번째 생성자를 위주로 살펴보자 !

파라미터 가운데에 corePoolSizemaximumPoolSize는 풀에서 스레드의 기본 개수와 최대 개수를 정한 것이다.
스레드 풀의 스레드 개수가 corePoolSize보다 커진다면, 초과하는 개수만큼의 태스크는 끝나고 나서 스레드를 유지할 필요는 없다.

Q. 그럼 corePoolSize만큼 코어스레드를 미리 생성하는 가?
-> 그렇지 않다

prestartCoreThread()라는 메서드가 별도로 있고 필요할 때 호출하면 된다.

ThreadPoolExecutor는 기본적으로 execute()submit()을 호출하는 순간에 작업 중인 스레드 개수가 corePoolSize보다 적으면 스레드를 새로 추가하는 형태이다.
keepAliveTime과 unit은 태스크가 종료될 때 바로 제거하지 않고 대기하는 시간이다.
보통 unit에는 TimeUnit.SECONDS / TimeUnit.MINUTES를 사용한다.

workQueue도 중요하다. 스레드 풀에서는 스레드를 corePoolSize 개수만큼 유지하려고 하고 추가로 요청이 들어오면 이 워크큐에 쌓는다.

workQueue 파라미터

workQueue에 쓸 수 있는 것은 3가지이다.

  • ArrayBlockingQueue
    • 큐 개수에 제한이 x
    • 요청이 들어오면 일단 큐에 쌓음
    • 큐가 꽉 찼을 경우 maximumPoolSize가 될 때까지 스레드를 하나씩 추가해서 사용

  • LinkedBlockingQueue
    • 일반적으로 큐 개수에 제한이 없음
    • 들어오는 요청마다 계속해서 쌓는데 maximumPoolsize 값은 의미 x
    • LinkedBlockingQueue도 LinkedBlockingQueue(int capacity) 생성자를 사용해 큐 개수를 제한할 수는 있다.

  • SynchronousQueue
    • 요청을 큐에 쌓지 않고 준비된 스레드로 바로 처리
    • 큐를 쓰지 않는다는 의미
    • 모든 스레드가 작업 중이라면? maximumPoolSize까지는 스레드를 생성해서 처리

handler 파라미터

ThreadPoolExecutor가 정지(shutdown)되거나, maximumPoolSize + workQueue 개수를 초과할 때는 태스크가 거부된다.
이 때 거부되는 방식을 정하는 것이 ThreadPoolExecutor 생성자의 마지막 파라미터인 RejectedExecutionHandlerhandler고, ThreadPoolExecutor의 내부 클래스에 미리 정의된 4개가 있다.

  • ThreadPoolExecutor.AbortPolicy
    디폴트 handler로 RejectedExecutionException 런타임 예외를 발생시킨다.

  • ThreadPoolExecutor.CallerRunsPolicy
    스레드를 생성하지 않고 태스크를 호출하는 스레드에서 바로 실행된다.

  • ThreadPoolExecutor.DiscardPolicy
    태스크가 조용히 제거된다.

  • ThreadPoolExecutor.DiscardOldestPolicy
    workQueue에서 가장 오래된 태스크를 제거한다.

RejectedExecutionHandler에 DiscardOldestPolicy 적용

앱에서 ThreadPoolExecutor를 사용할 때 가장 쓸모있는 리젝션핸들러는 DiscardOldestPolicy다.

리스트뷰, 스크롤뷰, 뷰플립버, 페이저 등에서 화면을 스크롤하면서 이동할 때, 이미 지나가버린 화면보다 새로 보이는 화면이 상대적으로 중요하다. 이 정책을 사용하면 오래된 것을 workQueue에서 제거하고 최신 태스크를 workQueue에 추가한다.

ScheduledThreadPoolExecutor 클래스

지연/반복 작업에 대해서는 이 실행 클래스를 사용할 수 있다. 앞에서 핸들러를 이용해도 지연/반복 작업을 할 수 있다고 했다. 화면 갱신이라면 Handler를 쓰는 게 적절하지만 백그라운드 스레드에서 네트워크 통신이나 DB 작업 등이 지연/반복 실행되는 경우는 ScheduledThreadPoolExecutor를 고려하는게 좋다.

반복/지연 작업의 다른 옵션으로 타이머를 생각할 수도 있지만 Timer API 문서에서도 ScheduledThreadPoolExecutor를 사용하도록 권장한다. Timer는 실시간 태스크 스케줄링을 보장하지 않고, 여러 스레드가 동기화없이 하나의 타이머를 공유하기 때문이다.

  • ScheduledThreadPoolExecutor도 ThreadPoolExecutor처럼 4개의 생성자가 있다.

이 클래스는 maximumPoolsize, keepAliveTime, unit, workQueue는 빠져 있다.
이 빠져 있는 4개의 파라미터는 ScheduledThreadPoolExecutor에서 고정되어 있다.

  • maximumPoolsize : Integer.MAX_VALUE
  • keepAliveTime : 0
  • workQueue : 내부 클래스인 DelayWorkQueue 인스턴스가 전달

DelayWorkQueue의 기본 사이즈는 16인데, 태스크가 많아지면 제한 없이 사이즈가 커진다.

Executors 클래스

ThreadPoolExecutor, ScheduledThreadPoolExecutor는 직접 생성하는 것보다는 Executors의 팩토리 메서드로 생성하는 경우가 많다. 팩토리 메서드 가운데서 용도에 맞는 게 없다면 결국 ThreadPoolExecutor, ScheduledThreadPoolExecutor 생성자를 직접 사용해야만 한다.

Executor부터 시작되는 클래스 다이어그램은 아래와 같다.

Executor에서 자주 쓰이는 팩토리 메서드들 또한 아래와 같다.

  • newFixedThreadPool(int nThreads)
    nThreas 개수까지 스레드를 생성
    workQeue 크기 제한 x

  • newCachedThreadPool()
    필요할 때 스레드 생성하는데, 스레드 개수에는 제한 x
    keepAliveTime은 60초로 길다. -> 이 때문에 Cached라는 수식어가 붙음

  • newSingleThreadExecutor()
    단일 스레드 사용해서 순차적 처리
    workQeue 크기 제한 x

  • newScheduledThreadPool(int corePoolSize)
    corePoolSize 개수의 ScheduledThreadPoolExecutor를 만든다.

Reference

  • 안드로이드 프로그래밍 <Next Step>
profile
와니와니와니와니 당근당근

0개의 댓글