2017년 10월 22일 일요일

[안드로이드] 스레드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++;
    }
}

명시적 잠금

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

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스레드의 실행시간을 보장하는 것을 권장한다.

댓글 없음:

댓글 쓰기