2017년 10월 22일 일요일

[안드로이드] 스레드2 - 안드로이드에서의 스레드 기초

스레드 통신

파이프

특징

  • 자바에서 제공하는 java.io 패키지의 API. 블로킹 IO를 지원한다.
  • 같은 프로세스 내의 두 스레드간의 단방향 데이터 전송을 지원
    • 리눅스의 파이프 통신 : 프로세스간 통신
    • 자바의 파이프 통신 : 동일 프로세스내의 스레드간 통신
  • 일반적인 사용케이스 : 하나의 태스크에서 다른 태스크로 계속해서 데이터를 옮길 때
  • 데이터 전송 유형
    • Binary : PipedInputStream / PipedOutputStream
    • String : PipedReader / PipedWriter
  • 파이프의 수명
    • 연결설정(connect()) : 시작
    • 연결종료(close()) : 종료

예제

데이터 생산자(UI 스레드)

  • EditText로부터 글자를 입력받아서, 처리할 스레드로 전송
public class TestActivity extends Activity implements TextWatcher {
    private EditText editText;
    private PipedWriter writer;
    private TextHandlerThread workerThread;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentVie(R.layout.activity_test);
        editText = (EditText) findViewById(R.id.edit);
        editText.addTextChangedListener(this);
        
        initializeConnection();
    }
    
    private void initializeConnection() {
        writer = new PipedWriter();
        workerThread = new TextHandlerThread();
        try {
            workerThread.connect(writer);
            workerThread.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        
        workerThread.interrupt();
        try {
            writer.close();
            workerThread.disconnect();
        } catch (IOExcepton e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        //추가되는 문자만 처리
        try {
            if (count > before) {
                writer.write(s.subSequence(before, count).toString());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void afterTextChanged(Editable s) {
    }
}

데이터 소비자(Worker 스레드)

  • UI 스레드로부터 전달받은 글자를 처리
public class TextHandlerThread extends Thread {
    private PipedReader reader;
    
    public TextHandlerThread() {
        reader = new PipedReader();
    }
    
    public void connect(PipedWriter writer) throws IOException {
        writer.connect(reader);
    }
    
    public void disconnect() throws IOException {
        reader.close();
    }
    
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                int i;
                while ((i = reader.read()) != -1) {
                    char c = (char) i;
                    //TODO : 문자처리 로직 추가
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

공유메모리

  • 공유 메모리 : 같은 프로세스내의 모든 스레드가 접근할 수 있는 메모리 영역(Heap 영역)
    • 인스턴스 멤버변수
    • 스태틱 멤버변수
    • 객체
  • 공유메모리를 이용해 스레드간 데이터 Read/Write - 각 스레드간의 동기화 문제가 발생할 여지가 있다!
    • 시그널링을 통해 해결

시그널링

  • 시그널링 : 특정 스레드의 상태를 다른 스레드로 전파
  • notify() / signal() - 무작위로 하나의 메서드만 꺠움
  • notifyAll() / signalAll() - 기다리는 모든 스레드를 깨움
동기화타입synchronizedReentrantLockReentrantReadWriteLock
Block & WaitObject.wait()
Object.wait(timeout)
Condition.await()
Condition.await(timeout)
Condition.await()
Condition.await(timeout)
NotifyObject.notify()
Object.notifyAll()
Condition.signal()
Condition.signalAll()
Condition.signal()
Condition.signalAll()
  • 안드로이드의 UI스레드에서는 사용할 수 없다.
    • UI스레드가 block 상태로 지속되면, ANR 발생

블로킹 큐

  • java.util.concurrent.BlockingQueue 클래스 API
  • 저수준 메커니즘인 시그널링 기법을 추상화하여 만든 고수준 메커니즘
    • 스레드 시그널링 + 리스트구현 래핑
  • 생산자 - 소비자 패턴을 이용해 문제 해결
    • put() : 큐에 넣는 메서드. 큐가 가득차면 block
    • take() : 큐에서 빼는 메서드. 큐가 비면 block

메시지 전달

  • Non-Blocking 생산자 - 소비자 패턴 메커니즘
  • android.os 패키지의 API를 이용
    • Message : 데이터 / 태스크를 포함하는 단위. 스레드간 주고받는 주체
    • MessageQueue : 소비자 스레드에서 처리할 메시지들을 담고있는 무제한의 연결리스트. 루퍼 당 하나의 스레드만 연결될 수 있다.
    • Looper : 메시지 발송담당. 스레드 당 하나의 루퍼만 연결될 수 있다.
    • Handler : 생산자 스레드에서의 메시지 큐잉, 소비자 스레드에서의 메시지 처리를 담당.

Message

  • 스레드간 통신을 위해 주고받는 주체
  • 데이터 or 태스크 중 하나만을 옮기는 컨테이너 객체
    • 데이터 : 소비자 스레드에서 처리됨
    • 태스크 : 소비자 스레드에서 동작하는 태스크. 현재 진행중인 태스크가 없을 때 실행됨
  • 소비자 스레드에서 메시지의 처리
    • 데이터가 포함된 메시지 : Handler의 handleMessage(Message) 콜백메서드에서 수신된 데이터 메시지를 처리
    • 태스크가 포함된 메시지 : 스레드에서 자동적으로 태스크를 실행. handleMessage() 콜백메서드에서는 태스크 메시지를 수신하지 않는다.
Message 내부 필드
필드명자료형설명
whatint메시지의 식별자
arg1, arg2int정수값을 전달하는 경우에 사용되는 간단한 데이터 필드
objObject임의의 객체. 이 객체가 다른 프로세스의 스레드로 전달될 때는 반드시 Parcelable 로 구현해야 한다.
dataBundle임의의 데이터값을 넣는 컨테이너
replyToMessanger다른 프로세스의 핸들러 참조. 프로세스간 통신을 위해 존재한다.
callbackRunnable스레드에서 실행할 태스크. Handler의 postXXX() 메서드에서 보낸 Runnable 객체
Message 생명주기
  • 4가지의 상태가 존재.
  • 앱에서 메시지를 다룰 때, 메시지의 상태에 어떠한 가정도 하지 않아야 한다.
  • 일단 메시지가 메시지큐에 추가되면, 메시지 안의 내용은 변경되면 안된다.

MessageQueue

  • 처리될 메시지들로 구성된 무제한의 단방향 연결리스트
  • 스레드에서는 핸들러를 이용하여 메시지큐 처리
    • 생산자 스레드 : 메시지큐에 메시지 삽입
    • 소비자 스레드 : 메시지큐에서 메시지 추출하여 처리
  • 메시지는 타임스탬프를 기준으로 정렬된다.
    • 가장 낮은 타임스탬프값을 가진 메시지가 맨 앞에 놓인다.(가장 빨리 처리된다.)
    • 현재시간보다 타임스탬프값이 작을 때만 해당 메시지를 소비자 스레드로 전달한다. 현재시간보다 타임스탬프값이 크다면(미래의 메시지), 해당 시각이 될 때까지 기다린 후 메시지를 전달한다.

Looper

  • 메시지큐에 있는 메시지를 정확한 핸들러로 보내는 역할을 담당
  • 소비자 스레드에서는 메시지큐에 직접접근을 하지 않고, 루퍼를 통해 메시지를 얻는다.
  • 루퍼는 스레드당 하나만 연결된다.
    • 이미 스레드와 연결된 루퍼를 다른스레드에 다시 연결하려고 하면 RuntimeException을 던진다.
    • 결과적으로, 스레드는 하나의 메시지큐만 가지는 것이 된다.
  • UI스레드를 제외한 모든 스레드는 메시지처리를 하기 위해서 명시적으로 루퍼를 얻어야 하고, 메시지 처리를 끝내려는 스레드는 명시적으로 루퍼를 종료해주어야 한다.
Looper 연결
public class ConsumerThread extends Thread {
    @Override
    public void run() {
        Looper.prepare();
        
        //TODO : 루퍼와 핸들러 연결
        
        Looper.loop();
    }
}
  • Looper.prepare() : 루퍼를 생성하고, 루퍼와 현재 스레드를 연결한다. 아직 이 단계에서 메시지를 전송하지는 않는다.
  • Looper.loop() : run()메서드가 완료되지 않았음을 보장하는 Blocking 메서드. run()메서드가 차단된 동안, 루퍼는 이 스레드로 메시지를 전달한다.
Looper 종료
  • quit() / quitSafely() 메서드
    • quit() : 현재시각보다 낮은 타임스탬프의 메시지를 포함하여, 모든 대기중인 메시지를 폐기
    • quitSafely() : 현재시각보다 낮은 타임스탬프의 메시지는 제외하고, 높은 타임스탬프의 대기메시지들을 폐기. 남은 메시지들은 루퍼가 종료되기 전에 처리된다. API레벨 18에서 추가된 메서드.
  • 루퍼를 종료한다고 스레드가 종료되지는 않는다. 단지, blocking 중이었던 loop() 메서드를 빠져나오는 것이다.
UI스레드의 루퍼
  • UI스레드는 앱이 초기화될 때 기본적으로 루퍼를 연결하기 때문에, 따로 연결할 필요가 없다.
  • UI스레드 루퍼는 종료시킬 수 없다. quit() / quitSafely() 메서드가 호출되면 RuntimeException을 던진다.
  • UI스레드 루퍼를 다른 스레드에 부착할 수 없다.
  • Looper.getMainLooper() 메서드를 이용하면 어디서든 UI스레드 루퍼를 접근할 수 있다.

Handler

  • 스레드에서 메시지 삽입/처리를 직접적으로 담당.
  • 핸들러는 내부적으로 메시지큐와의 상호작용을 하기위해 루퍼를 이용한다. 그러므로, 핸들러를 이용하는 스레드는 반드시 루퍼를 연결해야 한다.
Handler 생성
  • 생성시점에 루퍼와 바인딩된다. 루퍼와의 바인딩에 실패하면 RuntimeException을 던진다.
  • 한번 바인딩된 루퍼는 변경될 수 없다.
// 현재 스레드의 루퍼와 바인딩하는 생성자
new Handler();
new Handler(Handler.Callback);

//명시적으로 선언된 루퍼와 바인딩하는 생성자
new Handler(Looper);
new Handler(Looper, Handler.Callback);
메시지 삽입
//데이터 메시지 (sendXXX())
//메시지 객체 직접 전달
boolean sendMessage(Message msg)
boolean sendMessageAtFirstOfQueue(Message msg)
boolean sendMessageAtTime(Message msg, long uptimeMillis)
boolean sendMessageDelayed(Message msg, long delayMillis)
//메시지 의도만 전달
boolean sendEmptyMessage(int what)
boolean sendEmptyMessageAtTime(int what, long uptimeMillis)
boolean sendEmptyMessageDelayed(int what, long delayMillis)

//태스크 메시지 (postXXX())
boolean post(Runnable r)
boolean postAtFrontOfQueue(Runnable r)
boolean postAtTime(Runnable r, Object token, long uptimeMillis)
boolean postAtTime(Runnable r, long uptimeMillis)
boolean postDelayed(Runnable r, long delayMillis)
  • 메시지 전달순서 조절 - 앱이 유일하게 핸들링할 수 있는 방법이다.
    • 메시지의 처리시간은 불확정이다 - 기존에 처리중인 메시지 / 운영체제의 스케쥴링에 영향을 받기 때문
    • default : 전달이 즉시 가능
    • at_front : 타임스탬프값 0으로 전달되어, 다른메시지가 끼어들지 않는 한. 다음에 전달될 메시지가 된다.
    • delay : 이 지연시간 후에 메시지가 전달될 수 있다.
    • uptime : 이 절대시간에 메시지가 전달될 수 있다.
메시지 처리
  • 태스크 메시지
    • 따로 핸들링할 필요 없음. 전송된 태스크 Runnable의 run 메서드가 자동적으로 실행된다.
  • 데이터 메시지
    • 소비자 스레드에서 Handler.handleMessage(Message) 메서드를 오버라이드 하여 받은 메시지를 처리한다.
    • 소비자 스레드에서 Handler에 콜백 인터페이스를 등록한다.
//Handler.handlerMessage() 오버라이드
public class ConsumerThread extends Thread {
    public Handler handler;
    @Override
    public void run() {
        Looper.prapare();
        // 핸들러는 반드시 Looper가 설정된 다음에 생성해야 한다!!
        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                //메시지 처리
            }
        };
        Looper.loop();
    }
}

//Handler에 콜백 인터페이스 등록
public class ConsumerThread extends Thread implements Handler.Callback {
    public Handler handler;
    @Override
    public void run() {
        Looper.prapare();
        // 핸들러는 반드시 Looper가 설정된 다음에 생성해야 한다!!
        handler = new Handler(this);
        Looper.loop();
    }
    
    @Override
    public boolean handleMessage(Message msg) {
        //메시지 처리
        //더이상 처리할 메시지가 없을때 true 리턴. false를 리턴하면 Handler.handleMessage() 메서드가 이어서 호출된다.
        return true;
    }
}
메시지 제거
  • Handler의 메서드를 이용하면 큐에 넣어진 메시지를 제거할 수 있다.
    • 단, 메시지가 루퍼에 의해 큐에서 꺼내지기 전에만 가능하다.
    • 메시지를 정확하게 식별할 수 있어야 한다.
식별자 타입설명적용되는 메시지 타입
Handler메시지 수신자태스크 & 데이터 메시지
Object메시지 태그태스크 & 데이터 메시지
int메시지의 what 매개변수데이터 메시지
Runnable실행될 태스크태스크 메시지
// 메시지 큐에서 태스크 제거
void removeCallbacks(Runnable r)
void removeCallbacks(Runnable, Object token)

//메시지 큐에서 데이터 제거
void removeMessages(int what)
void removeMessages(int what, Object object)

//메시지 큐에서 태스크 / 데이터 동시에 제거(Object로 식별할 경우만 가능)
void removeCallbakcsAndMessages(Object token)


[안드로이드] 스레드1 - 스레드의 기초

스레드

스레드 기본

  • 태스크 : 프로그램에서 작업의 단위. 프로그래밍 언어 관점에서는 어떤 작업을 수행하기 위해 순차적으로 진행되는 코드들의 묶음으로 볼 수 있다.
  • 스레드 : 하나의 프로세서(CPU)에서 태스크의 실행
  • 하나의 프로세서는 한번에 하나의 명령어(코드 한줄)씩 처리할 수 있다.
    • 실행되는 스레드의 개수가 프로세서보다 많으면? 스케쥴러를 이용해 스레드를 바꿔가며 처리한다.
    • 스케쥴러는 스레드마다 가지고있는 우선순위를 이용해 처리순서를 조절한다.

싱글스레드 vs 멀티스레드 프로그램

싱글스레드

  • 장점
    • 스레드 처리로직이 없으므로 코드 복잡도가 낮다. - 코드 & 실행순서가 매우 간결함
  • 단점
    • 특정 동작이 오래걸릴 경우, 프로그램 전체가 지연되어 성능이 떨어진다.

멀티스레드

  • 장점
    • 오래걸리는 동작을 다른 스레드로 분리하여, 전체 프로그램은 지연되지 않으므로 성능이 떨어지지 않는다.
  • 단점
    • 스레드 처리로직이 추가되어야 하므로 코드 복잡도가 높다. - 실행순서를 예측하기 어려움
      • 자원 동기화 로직
      • 스레드 관리 로직
    • 메모리 등, 시스템 리소스의 사용량이 증가한다.

스레드 안전(Thread-Safety)

  • 여러 스레드에서 객체에 접근할 때, 객체가 항상 정확한 상태를 유지하는 상태

암시적 잠금

  • 자바의 synchronized 키워드를 사용
  • 필요 이상의 잠금영역을 만들면, 성능이 저하될 수 있다.
// 1.현재 메서드가 동작하는 객체의 메서드를 암시적으로 잠금
synchronized void changeState() {
    sharedResource++;
}

// 2.현재 메서드가 동작하는 객체의 특정 코드 블록을 암시적으로 잠금
void changeState() {
    synchronized (this) {
        sharedResource++;
    }
}

// 3.다른 객체를 이용하여 특정 코드 블록을 암시적으로 잠금
private final Object mLock = new Object();
void changeState() {
    synchronized (mLock) {
        sharedResource++;
    }
}

// 4.현재 메서드가 동작하는 클래스의 메서드를 암시적으로 잠금
synchronized static void changeState() {
    staticSharedResource++;
}

// 5.현재 메서드가 동작하는 클래스의 특정 코드 블록을 암시적으로 잠금
static void changeState() {
    synchronized (Myclass.class) {
        staticSharedResource++;
    }
}

명시적 잠금

  • 잠금을 세부적으로 컨트롤할 필요가 있을 때 주로 사용.
  • ReentrantLock, ReentrantReadWriteLock 클래스를 사용

ReentrantLock

  • 일반적인 Lock - synchronized 키워드와 동일한 효과
// ReentrantLock 사용
int sharedResource;
private ReentrantLock mLock = new ReentrantLock();

public void changeState() {
    mLock.lock();
    try {
        sharedResource++;
    } finally {
        mLock.unlock();
    }
}

ReentrantReadWriteLock

  • 공유메모리에 데이터를 Write 할때에만 잠금.
    • 읽기 스레드만 있을 때 : 잠금 동작 안함
    • 쓰기 스레드가 하나라도 있을 때 : 잠금 동작
  • 현재 스레드의 허용 / 차단여부를 확인해야 하므로, 단순한 잠금보다는 성능저하가 있을 수 있다.
// ReentrantReadWriteLock 사용
int sharedResource;
private ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();

//쓰기 메서드. 임계영역에 하나의 스레드만 접근하도록 잠금 수행
public void changeState() {
    mLock.writeLock().lock();
    try {
        sharedResource++;
    } finally {
        mLock.writeLock().unlock();
    }
}

//읽기 메서드. 여러 스레드에서 동시에 동작 가능
public int readState() {
    mLock.readLock().lock();
    try {
        return sharedResource;
    } finally {
        mLock.readLock().unlock();
    }
}

스레드 설계 전략

  • 태스크를 적당한 크기로 구성한다. 너무 큰 크기의 태스크라면, 쪼개는 것을 고려한다.
  • 태스크간의 실행순서를 파악한다.
    • 순차적인 실행이 필요한 태스크 : 하나의 스레드에서 순차적으로 수행
    • 순서에 무관한(동시 실행이 가능한) 태스크 : 여러 스레드에서 동시에 수행
  • 공유자원을 어떻게 관리할지 고려한다.

안드로이드에서의 스레드

  • 안드로이드는 리눅스를 기반으로 하므로, OS관점에서 모든 응용프로그램의 스레드는 리눅스의 pthreads를 기반으로 되어있다.

응용프로그램 스레드

  • 응용프로그램 관점에서의 스레드는 UI, 바인더, 백그라운드 의 3가지 유형으로 볼 수 있다.

UI 스레드

  • 응용프로그램의 메인스레드 - 응용프로그램의 프로세스와 같은 수명을 가짐
  • UI를 변경할 수 있는 유일한 스레드
    • UI요소는 이 스레드에서만 접근할 수 있으므로, 동시성문제의 영향을 받지 않는다.
  • UI스레드에서 처리하는 태스크가 많으면, UI 갱신(렌더링)이 느려지기 때문에 사용자의 입장에서는 반응성이 좋지 않다고 느끼게 된다.

바인더 스레드

  • 서로 다른 프로세스에서, 스레드사이의 통신에 사용
  • 다른 프로세스로부터 들어오는 요청을 처리하기 위해 만들어진다.
  • 일반적으로 플랫폼은 UI스레드를 먼저 사용하도록 다른 프로세스로부터 들어오는 요청을 변경하므로, 대부분의 경우에는 바인더 스레드에 대해 신경 쓸 필요가 없다.

백그라운드 스레드

  • 응용프로그램에서 명시적으로 생성하는 모든 스레드
  • UI 스레드로부터 파생된 스레드이므로, 특별한 설정변경이 없으면 UI 스레드의 속성을 상속받는다.
  • OS관점에서는 UI 스레드와 백그라운드 스레드는 같은 평범한 pthread 이므로 동등하게 처리된다.

리눅스 스레드 특징

  • 안드로이드는 리눅스 기반이므로, 안드로이드의 스레드는 리눅스의 스레드와 같다.

스케쥴링

  • 스레드의 개수가 프로세서보다 많은 경우, 프로세서는 스레드를 효율적으로 처리하기 위해 스레드를 스케쥴링한다.
  • 스케쥴링은 모든 응용프로그램의 스레드를 대상으로 이루어진다.
  • 스케쥴링에 영향을 미치는 요소로는 우선순위와 컨트롤그룹이 있다.

우선순위

  • 리눅스에서 사용하는 우선순위값을 그대로 이용한다.(나이스니스(niceness) 라고 불림)
  • 응용프로그램 관점에서는 자바에서 표현하는 우선순위를 변경하거나, 직접 나이스니스값을 변경하여 우선순위를 조정할 수 있다.
자바의 우선순위 이용
  • 자바에서 플랫폼 독립성을 구현하기 위해 제공하는 스레드 우선순위. 안드로이드 내부적으로는 이 우선순위의 값이 나이스니스값에 대응되어 사용된다.
  • 0 ~ 10 사이의 값.(10 : 최고 우선순위, 0 : 최저 우선순위)
  • 기본값은 5
  • java.lang.Thread 클래스의 setPriority(int priority) 메서드를 이용
  • ex)젤리빈에서의 자바 우선순위 - 나이스니스 대응 값
setPriority(priority)나이스니스
1 (Thread.MIN_PRIORITY)19
216
313
410
5 (Thread.NORM_PRIORITY)0
6-2
7-4
8-5
9-6
10 (Thread.MAX_PRIORITY)-8
나이스니스 값 이용
  • -20 ~ 19 사이의 값.(-20 : 최고 우선순위, 19 : 최저 우선순위)
  • 기본값은 0
  • android.os.Process 클래스의 setThreadPriority() 정적 메서드를 이용
    • setThreadPriority(int priority) : 이 메서드를 호출한 스레드의 나이스니스 변경
    • setThreadPriority(int threadId, int priority) : 해당 스레드의 나이스니스 변경

컨트롤 그룹

  • 그룹안에 속한 모든 스레드에 대해, 프로세서 시간할당 수준을 동일하게 관리하기 위해 사용하는 리눅스 컨테이너
  • 응용프로그램 관점에서는 foreground / background 그룹 2가지가 가장 중요하다.
    • foreground그룹의 스레드들은 background 그룹의 스레드들보다 프로세서의 실행시간을 길게 할당받는다. (background 그룹의 모든 스레드들의 실행시간을 합쳐도 5~10% 이내로 시스템이 제한)
    • 기본적으로, 사용자의 눈에 보이는 프로세스에 속한 스레드는 foreground 그룹에 속하도록 시스템이 조절한다.
    • 이를 통해 눈에 보이는 응용프로그램에 대한 성능을 보장함
  • 현재 눈에 보이는 응용프로그램에서 스레드를 생성하면, 그 스레드는 UI스레드와 같은 우선순위 / 같은 컨트롤 그룹을 가지게 되어, UI스레드의 성능을 감소시킬 여지가 있다.
    • Process.setThreadPriority() 메서드를 이용하여 Process.THREAD_PRIORITY_BACKGROUND 우선순위로 낮추면, 스레드의 우선순위가 낮아짐과 동시에, 항상 백그라운드 그룹에 속하게 된다. 이를 이용하여, UI스레드의 실행시간을 보장하는 것을 권장한다.