요점정리

안드로이드 멀티스레딩(Efficient Android Threading)

dextto™ 2015. 8. 5. 22:18

안드로이드 개발자의 필독서라 할 수 있겠다.

일반 자바 동시성 프로그래밍에서 부터 안드로이드 비동기 프레임웍까지 두루 다뤄준다.


JAVA8의 Collection#parallelStream을 이용하면 코드양을 획기적으로 줄일 수 있다.

하지만 이전 기술이라고 무시하지 말고 알아두자. 이전에 작성된 코드를 읽을 일도 많이 있을 테니까.




4.4.7 메시지 큐 관찰

메시지 전달을 자세히 살펴보는 방법


1. MessageQueue에 Handler가 추가한 메세지 스냅샷 찍기 

:    handler.dump(new LogPrinter(Log.DEBUG, TAG), "");

(참고) 전달경계를 넘었지만 큐에 남아 있는 메시지는 마이너스 시간으로 표시됨


2. 메시지 큐의 처리 추적

:   Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, TAG));



5.2.2 비동기식 RPC

AIDL 비동기 호출 구현방법

- .aidl 파일의 interface 또는 메소드 선언부에 oneway 키워드를 추가

- 메소드의 인자로 콜백 객체를 받도록 하자


5.3 바인더를 이용한 프로세스간 메시지 전달

: 서버/클라이언트 각각에 생성한 Messenger를 이용한다!



Chap. 8 HandlerThread

mHandler = new Handler(handlerThread.getLooper());

==> 스레드가 시작되었다면 루퍼가 초기화되어 메시지를 받을 수 있을 때까지 block 시킨다. 이렇게 경쟁조건 문제(4.4.1참고) 해결


HandlerThread는 다음의 편리한 속성으로 태스크 연쇄를 위한 구조를 제공한다

- 설정이 용이

- 연쇄로 묶일 수 있는 독립적이고 재사용 가능한 태스크

- 순차적 실행

- 연쇄 안에서 다음 태스크로 연결할지 말지 결정해야 하는 자연(?) 결정 지점

- 현재 상태 보고

- 태스크 연쇄 내 하나의 태스크에서 다른 태스크로의 쉬운 데이터 전달



Chap. 9 Executor 프레임워크를 통한 스레드 실행 제어

안드로이드 비동기 기술의 토대.

AOSP에서 제공하는 비동기 기술로 원하는 실행 동작을 제어하지 못한다면, 실행의 모든 제어를 제공하는 Executor 프레임워크를 사용하자.


9.2.3 스레드 풀 설계

풀의 최대 크기 선정: 

- 너무 적으면? : 충분한 속도로 큐에서 태스크를 꺼내지 못한다. 모든 스레드가 긴 I/O를 연산을 할 경우, I/O 동작이 완료될 때까지 실행 시간을 얻지 못하고 큐에서 기다리는 짧은 수명의 스레드가 있을 수 있다.

- 너무 많으면? : 스레드 전환에 많은 시간을 사용한다.

- 추천개수: N(CPU의 개수) + 1 또는 2*N개

   int N = Runtime.getRuntime().availableProcessors()

- 작업 스레드의 개수보다 많게 하지 말것. 유휴 스레드가 생겨 자원 낭비.

- 같은 스레드 풀에서 서로 종속된 태스크(공통 상태를 공유하거나, 실행 순서가 있을 때)를 실행할 경우, 모든 스레드가 점유되어 있으면 교착상태에 빠질 수 있다. 이를 고려해서 개수를 정해야 함.


9.2.5 스레드 풀의 중단

void shutdown(): 새로운 태스크 거부, 대기중인 태스크는 처리

List<Runnable> shutdownNow(): 새로운 태스크 거부, 대기중 태스크 실행하지 않고 다른 스레드에서 실행할 수 있도록 리스트로 반환


0개의 핵심 풀의 위험

핵심 풀: 스레드 풀에 포함되는 스레드 개수의 하한. 실제로 스레드 풀은 0개 스레드로 시작하지만, 핵심 풀 크기에 도달하면 하한 이하로 떨어지지 않는다.

이를 0으로 지정할 경우, 예를 들어 0개의 핵심 스레드와 10개 태스크를 보유할 수 있는 제한된 큐에서는, 스레드 생성을 싲가하는 11번째 태스크가 삽입될 때까지 어떤 태스크도 실행되지 않는다.


9.3 태스크 관리

Callable과 Future 인터페이스


public interface Callable<V> {

    public <V> call() throws Exeption;

}


ExecutorService#submit를 이용하여 Callable 태스크가 처리되면 Future로 처리한다.


Future에 의해 제공되는 메서드

boolean cancel(boolean mayInterruptIfRunning)

V get() : 결과를 얻을 때까지 기다린다.

V get(long timeout, Timeunit unit): 결과를 얻을 때까지 시간만큼 기다린다.

boolean isCancelled()

boolean isDone()


ExecutorService#invokeAll : 

- 동시에 여러 개의 독립적인 태스크를 실행한다.

- 비동기 계산이 완료되거나 시간제한이  만료될 때까지 스레드 호출을 차단하여 응용프로그램이 모든 태스크가 완료되기를 기다리게 한다. 

- 입력된 순서와 같은 순서로 결과를 Future 리스트에 저장한다.

- 사용예: 두 개의 서로 다른 위치에 있는 네트웍 데이터를 가져와 취합하는 경우


ExecutorService#invokeAny :

- 첫 번째로 마친 태스크에서 결과를 반환한 다음 태스크의 나머지 부분은 무시한다.

- 서로 다른 여러 데이터 집합에 걸쳐 검색하다가 검색 결과가 발견되자마자 즉시 중지하고자 할 경우

- 또는 병렬로 실행되고 있는 태스크들 중에서 하나의 결과만 원할 경우


ExecutorCompletionService :

완료된 결과의 관리. 블로킹 큐를 기반으로 한 '완성 큐'를 가지고 있다.



Chap 10. AsyncTask로 백그라운드 태스크를 UI 스레드에 묶기

AsyncTask는 전역 실행 환경.


AsyncTask#cancel(boolean mayInterrupIfRunning)

: 취소시, 인자에 따라 task 스레드에 인터럽트를 걸 것일지 말 지 결정한다.


상태값: PENDING, RUNNING, FINISHED

           이 값을 이용해서 한 번에 하나의 태스크만 실행할 지 결정할 수 있다.


구현시 메모리 릭이 발생하지 않도록 주의!!

- 6.2절 [스레드 관련 메모리 누수]의 설명과 같이 작업자 스레드가 살아 있는 한 모든 참조 객체는 메모리에 유지된다.

- 따라서 AsyncTask를 독립형(??)이나 정적 내부 클래스로 선언한다. 보통 Context를 참조하는 정적 내부 클래스로 선언한다.


10.3.2 다양한 플랫폼 버전에서 실행

execute의 실행방식은 targetSdkVersion에 달라진다. 문서를 잘 읽어보고 구현할 것!!

executeOnExecutor: 여러 스레드에서 동시 실행할 때 사용. API 11부터 지원.


일관된 동시 실행을 위한 래퍼 클래스

public class ConcurrentAsyncTask {

    public static void execute(AsyncTask as, Object[] params) {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR2) {

            as.execute(params);

        } else {

            as.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);

        }

    }

}


10.3.3 커스텀 실행

AsyncTask에 미리 정의된 실행자 (SERIAL_EXECUTOR, THREAD_POOL_EXECUTOR)는 App 전역에 걸쳐 적용되므로, App이 많은 태스크를 실행할 때는 성능 저하의 위험이 있다. 이를 피하려면 Custom Executor로 대체한다.

Executors#newSingleThreadExecutor로 생성해서 인자로 넘기자.


AsyncTaks생성시 매개변수가 없거나(모두 Void),  doInBackground메서드만 구현하는 경우는 단순히 백그라운드로 작업하기 위한 것이므로, HandlerThread를 이용하자


작업자 스레드와 Looper 연결해서 메시지를 전달하길 원한다면 HandlerThread를 사용.


Local Service 내에 백그라운드 스레드를 사용할 때

: Thread, Executor 프레임워크, HandlerThread, 커스텀 실행자를 가진 AsyncTask 중 선택해서 사용



Chap 11. 서비스(를 통한 비동기 실행)


11.6 바운드 서비스

- 지역 바인딩 (11.6.1절)

- Messenger를 이용한 원격 바인딩 (5.3절)

- AIDL을 이용한 원격 바인딩 (5.2절)


11.7 비동기 기술 선정

- 태스크가 순차적으로 실행될 때는 IntentService(12장)를 사용하는 것이 좋다. IntentService는 서비스를 종료시키는 순차적 태스크 실행용 지원을 내장(?? 12장 읽고 문구 수정)하고 있기 때문이다.

- 같은 프로세스에서 다른 component로서 실행되는 AsyncTask는 비전역 실행자 같은 Custom Executor를 사용하거나 Thread pool 처럼 대체 기술을 사용해야 한다.



Chap 12. IntentService

서비스 생명주기의 속성 + 백그라운드 스레드의 태스크 처리를 내장

싱글 백그라운드 스레드에서 태스크를 실행. 즉, 모든 태스크는 순차적으로 실행됨.

11.5.4 태스크 제어 서비스와 같이 활성화된 component를 항상 포함하므로, 너무 일찍 태스크를 종료하는 위험을 줄여준다.

구현과 구조가 이 간단하므로 서비스를 이용하기 전에 먼저 고려해 보자.



사용예1 : 12.2.1. 순차적으로 정렬된 태스크

ex) 웹 서비스 get/post 통신


사용예2 : 12.2.2. 브로드캐스트 리시버에서 비동기 실행

BroadcastReceiver로 앱이 시작될 때 프로세스가 빈 상태(?)라면 런타임이 프로세스를 죽일 수 있다. 그러면 태스크의 결과는 손실된다. 이를 회피할 때 쓰자.

ex) AlarmManager를 이용한 주기적인 작업



Chap 13. AsyncQueryHandler를 이용한 ContentProvider 접근

AsyncQueryHandler: ContentResolver, 백그라운드 실행, 스레드 간 메시지 전달을 처리함으로써 ContentProvider에 대한 비동기 접근을 간소화 해주는 추상 클래스.


Token: 요청 유형(식별자)

Cookie: 요청 식별자 및 임의 객체 유형의 데이터 컨테이너


startQuery, startInsert, startUpdate, startDelete


참고

- ContentProviderOperation

- CancellationSignal: AsyncQueryHandler는 이를 지원하지 않음.  cancelOperation(token)을 사용해야 함.


CursorLoader와 조합하여 사용하는게 이상적이다! (14장 예제 참조)

데이터 쿼리 ==> CursorLoader사용

삽입/업데이트/삭제 ==> AsyncQueryHandler 사용



Chap 14. Loader를 이용한 자동 백그라운드 실행


Loader Framework

: 클라이언트(액티비티 / 프래그먼트)와 함께 동작하는 DB관리 프레임워크

: LoaderManager, Loader(AsyncTaskLoader, CursorLoader)로 구성

: AsyncTaskLoader를 상속받아 CursorLoader대신 커스텀 로더로 많이 사용


로더 프레임워크의 기능

- 비동기 데이터 관리

- 생명주기 관리: 클라이언트가 멈추면 함께 멈춘다. orientation은 변경해도 계속 진행된다

- 데이터 캐시

- 누수 보호: Application Context에서만 작동


LoaderManager#initLoader와 LoaderManager#restartLoader의 차이점

: 클라이언트 상태가 바뀔 때 캐시(로드되어 있는 데이터) 사용 여부


AsyncTaskLoader를 쓰면 비동기로 동작 가능

이는 10.3절의 안드로이드 버전별 동작 차이의 문제가 없다.

반응형