본문 바로가기
프로그래밍 놀이터/안드로이드, Java

Efficient Android Threading #2 스레드 통신

by 돼지왕 왕돼지 2018. 3. 18.
반응형

Efficient Android Threading #2 스레드 통신


이 글은 Efficient Android Threading 의 일부 내용만 발췌한 내용입니다.

자세한 내용은 책을 구입해서 보세용.

addIdleHandler, api level, arg1, arg2, blocking, Callback, Consumer, data, data message, data transfer, delay, disconnection, DispatchMessage, dump, Efficient Android Threading #2 스레드 통신, fail safe, flush, getmainlooper, handler log, handler.post, IdleHandler, linkedblockingqueue, LogPrinter, looper, looper handler binding, looper log, looper.loop, looper.prepare, looper.quit, looper.quitsafely, message, message 재사용, Message.obtain, MessageQueue, messenger, myqueue, obj, overhead, pipe lifecycle, pipedinputstream, pipedoutputstream, pipedreader, pipedwriter, prepareMainLooper, Producer, producer consumer, queueIdle, Read, Reader, removeIdleHandler, replyto, return 값, Runnable, RuntimeException, setMessageLogging, setup, task message, thread message queue relationship, ui 스레드와 통신, uptime, Wait, What, write, writer, 객체 저장, 공유 메모리, 다른 스레드, 단방향, 데이터 전송, 두개의 스레드, 메모리 버퍼, 문자 데이터, 바이너리 데이터, 분리, 불확정적, 블로킹 큐, 새로운 루퍼, 설정, 소비자 스레드, 스레드 시그널링, 스레드 안전, 스케줄링, 실패 예외 처리, 쓰기, 쓰기 close, 안드로이드 메시지 전달, 읽기, 읽기 close, 컨슈머, 타임 슬롯, 파이프, 파이프 생명 주기, 프로듀서, 호출 스레드


4.1. 파이프


-

파이프는 두 개의 연결된 스레드끼리만 접근할 수 있는 메모리에 할당된 버퍼다.

두 개의 스레드 이외의 다른 스레드는 데이터에 접근할 수 없다.

따라서 스레드 안전이 보장된다.

파이프는 단방향이기 때문에 한 스레드는 쓰기만 하고 다른 하나는 읽기만 한다.


일반적으로 파이프는 두 개의 긴 실행 테스크가 있고 하나의 테스크에서 다른 테스크로 계속해서 데이터를 옮길 때 사용된다.



-

파이프는 바이너리 데이터와 문자 데이터 중 하나를 전송할 수 있다.

PipedOutputStream(생산자)와 PipedInputStream(소비자)은 바이너리 데이터 전송을 나타내고,

PipedWriter(생산자)와 PipedReader(소비자)는 문자 데이터 전송을 나타낸다.

파이프의 수명은 쓰기(Writer) 스레드 또는 읽기(Reader) 스레드 중 하나가 연결을 설정하면 시작되고, 연결이 닫히면 종료된다.




** 4.1.1. 기본 파이프 사용


-

파이프 생명주기는 설정(setup), 데이터 전송(data transfer), 분리(disconnection)의 세 단계로 요약된다.


1. 연결 설정

PipedReader r = new PipedReader();

PipedWriter w = new PipedWriter();

w.connect(r); // r.connect 도 가능하다.



2. 처리하는 스레드로 읽기(PipedReader) 전달.

Thread t =  new MyReaderThread(r);

t.start();



3. 데이터 전송

w.write(‘A’);

w.flush();


// in MyReaderThread

int i;

while((i = reader.read()) != =1){

    char c = (char) i;

}



4. 연결 닫기.

w.close();

r.close();



-

통신은 차단(blocking) 메카니즘을 가진 소비자-생산자 패턴을 따른다.

파이프가 가득 차면, 쓰기 스레드가 추가할 데이터를 위한 공간을 확보하기에 충분한 만큼의 데이터를 읽을 때까지 write() 메서드는 차단된다.

read() 메서드는 파이프에서 읽을 수 있는 데이터가 없을 때마다 차단된다.


flush() 는 새로운 데이터가 도착했음을 소비자 스레드에 알려준다.

보통 버퍼가 비었을 때 PipedReader 는 1초 간격을 두고 wait() 로 차단을 요청하기 때문에, flush() 호출이 생략되면 소비자 스레드는 최대 1초까지 데이터 읽기가 지연될 수 있다.



-

쓰기와 읽기가 연결되어 있을 때는 둘 중 하나만 닫는 것으로 충분하다.

쓰기를 닫으면 파이프는 분리되지만 버퍼 안의 데이터는 여전히 읽을 수 있다.

읽기를 닫으면 버퍼가 지워진다.





4.2. 공유 메모리





4.3. 블로킹 큐

-

스레드 시그널링은 정교한 설정이 가능한 저수준의 메커니즘이므로 많은 사용 사례에 적용할 수 있지만, 가장 오류가 발생하기 쉬운 방법으로 간주되기도 한다.



-

대표적인 블로킹 큐는 LinkedBlockingQueue 이다.





4.4. 안드로이드 메시지 전달


-

처리할 메시지가 없을 때 소비자 스레드는 일부 유휴 시간을 갖는다.

기본적으로 소비자 스레드는 유휴 시간 동안 새로운 메시지를 단순히 기다린다.

그러나 이러한 유휴 슬롯 동안 대기하는 대신, 다른 작업을 수행하기 위해 스레드를 이용할 수 있다.


IdleHandler 인터페이스로 이 타임 슬롯을 활용할 수 있다.

MessageQueue mq = Looper.myQueue();

MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler();

mq.addIdleHandler(idleHandler);

// mq.removeIdleHandler(idleHandler);



-

IdleHandler 는 하나의 콜백 메서드를 갖는다.

interface IdleHandler{

    boolean queueIdle();

}


메시지 큐는 소비자 스레드에서 유휴 시간을 감지하면 등록된 모든 IdleHandler 인스턴스에 queueIdle() 메서드를 호출한다.

오랜 시간 실행되는 테스크는 실행 시간 동안 메시지를 지연시키기 때문에 일반적으로 피해야 한다.


return 값이 true 라는 것은 IdleHandler 가 활성화 상태라는 것으로, 계속해서 유휴 시간에 콜백을 수신한다는 의미이다.

false 는 비활성화 상태로, 유후 시간에 더 이상 콜백을 받을 수 없다.

messageQueue.removeIdleHandler() 를 통해 리스너를 제거하는 것과 같다.



-

메시지는 다음의 매개변수들을 갖는다.


what ( int )

arg1, arg2 ( int )

obj ( Object )

data ( Bundle )

replyTo ( Messenger )

callback ( Runnable ) : 내부 인스턴스 변수, Handler.post 를 통해 전달한 Runnable 을 담는다.



-

데이터 메시지의 경우 메시지가 전달되지만,

테스크 메시지의 경우 메시지는 전달되지 않고 Runnable 이 그대로 처리된다.



-

런타임은 이전 메시지를 재사용하기 위해 응용프로그램 전체 풀에 메시지 객체를 저장한다.

메시지를 재사용하면 메시지 전달을 위해 매번 새로운 인스턴스를 생성하는 오버헤드를 피할 수 있다.

매시지 객체의 실행 시간은 일반적으로 매우 짧고, 많은 메시지가 단위시간에 처리된다.






-

// 명시적 객체 생성

Message m = new Message();


// 팩토리 메서드

// 빈 메시지

Message m = Message.obtain();


// 데이터 메시지

Message m = Message.obtain(Handler h, int what, int arg1, int arg2, Object o); // 다른 형태 많음


// 테스크 메시지

Message m  = Message.obtain(Handler h, Runnable task);


// 복사 생성자

Message m = Message.obtain(Message originalMsg);



-

일단 메시지가 큐에 삽입되면 메시지 안의 내용은 변경되어서는 안 된다.

이론적으로 메시지가 전달되기 전에 내용을 변경하는 것은 유효하다.

그러나 상태를 확인할 수 없으므로, 생산자 스레드가 데이터를 변경하는 동안 소비자 스레드가 메시지를 처리할 수도 있고, 이 경우 스레드 안전 문제가 일어날 수 있다.

메시지가 재활용되면 문제는 더 심각해진다.

그러므로 큐에 넣은 후에는 바로 관리 권한을 놓아야 한다.



-

Looper.prepare() 는 메시지 큐를 만들고 현재 스레드와 메시지 큐를 연결한다. 이 단계가 끝나고 생산자는 Message 를 queue 에 넣을 수 있지만, 소비자에게 전달되진 않는다.


Looper.loop() 은 메시지 큐에서 메시지를 처리하기 시작한다. 루퍼가 메시지를 소비자 스레드로 전달한다.



-

스레드는 오직 하나의 관련 루퍼를 가질 수 있다.

앱이 이미 설정된 다른 루퍼를 설정하려고 하면 런타임 에러가 발생한다.

결과적으로 스레드는 오직 하나의 메시지 큐만 가지며, 이는 여러 생산자 스레드에서 보내진 메시지들이 소비자 스레드에서 차례대로 처리됨을 의미한다.



-

Looper.quit() 는 더 이상 메시지를 전달받지 않고 전달 경계를 넘은 모든 메시지 포함하여 모든 메시지가 폐기된다.

Looper.quitSafely() 는 전달 경계를 넘지 못한 메시지만을 큐에서 폐기한다. 전달 가능한 대기 메시지는 루퍼가 종료되기 전체 처리된다.

( quitSafely 는 API Level 18, 젤리빈 에서 추가되었다. )


Looper 를 종료한 후에는 loop() 을 호출한 이후의 코드들이 수행된다.

해당 스레드에서 새로운 루퍼를 시작할 수 없다.

혹여 Looper.loop() 을 호출하더라도 blocking 은 되지만 메시지가 실제 전달되지는 않는다..



-

UI 스레드의 루퍼는 다른 루퍼와 차이가 있다.


1. Looper.getMainLooper() 메서드를 이용하면 어디서든 접근할 수 있다.

2. 종료시킬 수 없다. Looper.quit() 메서드를 호출하면 RuntimeException 이 발생한다.

3. 런타임은 Looper.prepareMainLooper() 로 UI 스레드에 루퍼를 연결한다. 이 함수는 앱당 한번만 수행할 수 있다. 따라서 메인 루퍼를 다른 스레드에 부착하려 하면 예외가 발생한다.



-

핸들러가 한 루퍼에 바인딩되면 이 바인딩은 다른 루퍼로 변경할 수 없는 최종 바인딩이다.



-

한 개의 Looper 를 공유하는 여러 개의 핸들러는 동시 실행이 가능하지 않다. 메시지는 같은 큐에 있고, 차례대로 처리된다.



-

Handler 는 Message 클래스 객체 생성 관련 팩토리 메서드들을 가지고 있다.



-

명시적인 delay 나 uptime 이 지정되어도 각 메시지의 처리 시간은 여전히 불확정적이다.

처리 시간은 먼저 처리해야 하는 기존의 메시지들과 운영체제 스케줄링에 좌우된다.



-

큐에 메시지를 삽입하는 것은 fail safe 한 작업이 아니므로 실패에 대비하여 예외 처리를 할 필요가 있다.


메시지에 핸들러가 없을 때, RuntimeException 이 발생한다. 이는 지정된 핸들러 없이 Message.obtain() 메서드로 메시지가 생성되었을 때 발생한다.

메시지가 이미 전달되었거나 처리 중인 경우 RuntimeException 이 발생한다. 같은 메시지 인스턴스가 두 번 삽입되었을 경우 발생한다.

루퍼가 종료되었을 때는 false 가 return 된다. Looper.quit() 이후에 메시지를 삽입하면 그렇다.



-

Handler 클래스의 dispatchMessage 메서드는 소비자 스레드에 메시지를 전달하기 위해 루퍼에 의해 사용된다.

앱에서 호출할 경우 메시지는 소비자 스레드가 아닌 호출 스레드에서 즉시 처리된다.



-

Handler 에 전달하는 Callback 에서 false 를 반환하면, Handler 내부의 handleMessage 가 한번 더 불린다.

물론 true 를 반환하면, Callback 에서의 처리로 메시지 처리를 완료한다.



-

메시지는 자신이 어떤 핸들러로 전달되는지 항상 알고 있다.

따라서 핸들러 식별자는 모든 메시지의 필수 요구사항이다.

이러한 요구사항은 암시적으로 각 핸들러가 자신에 속한 메시지만을 제거할 수 있도록 제한한다.



-

아래 명령어를 통해 handler 에 대한 정보를 얻을 수 있다.

handler.dump(new LogPrinter(Log.DEBUG, TAG), “”);


아래 명령어를 통해 Looper 에 대한 정보도 얻을 수 있다.

looper.setMessageLogging(new LogPrinter(Log.DEBUG, TAG));




4.5. UI 스레드와 통신









반응형

댓글