brunch

You can make anything
by writing

C.S.Lewis

by 이승현 Feb 27. 2017

Threading Performance #04

HandlerThread, Thread priority

#01 Round-trip model


스레드 작업의 경우 대부분 왕복 모델(round-trip model)을 사용합니다.

UI 스레드에서 어떤 일을 다른 스레드에 넘긴 후, 다시 UI 스레드에서 받아 처리하는 작업.


이러한 경우 AsyncTask가 가장 적합하지만, 그렇지 않은 경우도 있습니다.



#02 Camera Preview


카메라에서 미리보기 프레임을 가져 오기를 생각해봅시다.

미리보기 프레임들은 onPreviewFrame() callback을 통해 전달되고, 이 callback은 camera.open() 메서드가 호출된 동일한 스레드에서 호출되어야 합니다.



#03 MainThread


UI 스레드에서 이 callback을 호출하면 아주 큰 픽셀 배열을 처리하는 작업이 UI 렌더링 및 App 이벤트 처리 작업을 방해해 앱 성능에 영향을 끼칩니다.

메인 스레드(UI 스레드)에서 호출하기에는 적합하지 않습니다.



#04 AsyncTask


AsyncTask도 마찬가지입니다.

OS 버전에 따라 다르지만, 허니컴 이상부터는 단일 백그라운드 스레드에서 순차적으로 실행됩니다.

execute()
this function schedules the task on a queue for a single background thread or pool of threads depending on the platform version.

즉, 시간이 오래 걸리는 작업을 하면 다른 AsyncTask 작업에 영향을 주어, 결국 UI 렌더링 및 App 이벤트 처리 작업에 영향을 미칩니다.




HandlerThread


#05 HandlerThread


이러한 문제들을 해결하기 위해, callback들을 수신하고, 이를 오랫동안 처리할 수 있는 non-UI, non-AsyncTask 스레드가 필요합니다.


안드로이드는 HandlerThread라는 Helper 클래스를 제공하고 있습니다.


HandlerThread는 Looper를 가진 장기 실행 스레드입니다.

따라서 스레드를 live 상태로 유지하고, Message Queue를 가진 Looper와, Looper가 Message를 처리할 수 있도록 Message Queue에 Message를 추가하는 Handler를 생성할 수 있습니다.




#06 HandlerThread with onPreviewFrame() callback


HandlerThread에서 camera.open()과 onPreviewFrame() callback을 수신해서 처리할 수 있습니다.

UI 스레드나 AsyncTask에서 발생한 문제들도 사라지게 됩니다.

그리고 runOnUiThread를 통해 작업 결과를 UI 스레드에서 처리할 수도 있습니다. 

간단하게 아래와 같이 구현할 수 있습니다.


시간이 오래 걸리는 non-UI 작업은 HandlerThread가 적합합니다.

#07 HandlerThread codes




Thread priority


#08 Thread priority


CPU가 한 번에 처리할 수 있는 스레드의 개수는 정해져 있습니다.

따라서 시스템의 스레드 스케줄러는 중요한 스레드에 높은 우선순위를 부여하고, 균형 있게 유지하여 결국 모든 작업을 완료해야 합니다.


HandlerThread를 포함하여 모든 스레드를 만들고 관리할 때, 올바른 우선순위를 갖도록 설정하는 것이 중요합니다. 

너무 높게 설정하면 스레드가 UI 스레드와 RenderThread를 중단시켜 앱이 프레임을 떨어 뜨릴 수 있고, 너무 낮게 설정하면 비동기 작업 (이미지 로드 등)이 느려질 수 있습니다.


HandlerThread를 만들 때 수행 중인 작업의 유형에 따라
우선순위를 할당해야 합니다.



우선순위는 두 가지 방식으로 할당됩니다.

안드로이드는 앱의 생명 주기(life cycle) 상태를 기반으로 스레드 우선순위를 할당합니다.

#09 Thread priority based on the life cycle


화면에 보이는 앱(Active visible apps)들은 foreground 그룹에 할당되고, 나머지는 background에 할당됩니다. 그리고 foreground 그룹의 스레드는 디바이스 총 실행 시간의 약 90% ~ 95%를 차지하고, background 그룹은 약 5% ~ 10% 만 차지합니다.


즉, 화면에 보이는 앱들은 보이지 않는 앱들보다 많은 프로세서 시간을 할당받아, background 스레드는 foreground 스레드에 영향을 미치지 않고 작업을 수행할 수 있고, foreground 스레드도 문제없이 실행됩니다.




안드로이드는 스레드의 우선순위를 spawning 스레드(생성하고 실행하는 스레드)와 동일한 우선순위 및 그룹 멤버십으로 설정합니다.

따라서 foreground 그룹의 스레드는 동일한 우선순위를 가지고 프로세서 시간 할당에 대해 동일한 조건으로 경쟁합니다.


#10 Same priority




만약 우선순위가 높은 작업 스레드를 생성하면, CPU 리소스를 모두 똑같이 경쟁하게 되므로 16ms 안에 모든 UI 작업을 처리할 수 없게 될 수 없게 되어, 스레딩 성능 이슈가 발생합니다.


#11 High priority work threads




이 문제를 해결하려면 setThreadPriority()를 사용하여
스레드 우선순위를 명시 적으로 설정할 수 있습니다.



#12 Process.setThreadPriority(int priority)


HandlerThread에서는 생성자를 통해 우선순위를 설정할 수 있습니다.

만약 생성자를 통해 우선순위를 설정하지 않는다면 기본 우선순위로 설정됩니다.

#13 HandlerThread priority




Process 클래스는 앱이 스레드 우선순위를 설정하는 데 사용할 수 있는 상수 세트를 제공하여 우선순위 값을 할당하는 복잡성을 줄이는 데 도움이 됩니다.

가장 높은 우선순위 THREAD_PRIORITY_URGENT_AUDIO에서부터 가장 낮은 우선순위인 THREAD_PRIORITY_LOWEST 까지 제공하고 기본 우선순위는 THREAD_PRIORITY_DEFAULT입니다.





HandlerThread 에 대한 예제를 찾아보려 했는데 책에도 없고 구글링해도 거의 없습니다...

시간이 오래 걸리는 non-UI 작업을 위해 사용하길 권장하는데, Thread를 상속받아 구현하다 보니 우선순위도 설정해야 하고 이것저것 많이 불편하네요. (안 쓰는 이유가...)


좀 더 자세한 내용은 아래 링크와 영상을 보시기 바랍니다.


브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari