Android Thread의 이해

2019. 9. 27. 23:10

Android Thread의 이해

 

 

Program, Process 그리고 Thread

 

 

프로그램(Program)

 

  • 사전적 의미: 어떤 작업을 위해 실행할 수 있는 파일

 

 

프로세스(Process)

 

  • 사전적 의미: 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램
  • 메모리에 올라와 실행되고 있는 프로그램의 인스턴스(독립적인 개체)
  • 운영체제로부터 시스템 자원을 할당받는 작업의 단위
  • 즉, 동적인 개념으로는 실행된 프로그램을 의미
  • CPU 시간, 운영되기 위해 필요한 주소 공간, Code, Data, Stack, Heap 구조로 되어 있는 독립된 메모리 공간
  • 기본적으로 프로세스당 최소 1개의 스레드(메인 스레드)를 소유
  • 각 프로세스는 별도의 주소 공간에서 실행되며, 한 프로세스는 다른 프로세스의 변수와 자료구조에 접근 불가
  • 한 프로세스가 다른 프로세스의 자원에 접근하려면 프로세스 간의 통신(IPC, inter-process communication)을 사용

 

 

스레드(Thread)

 

  • 사전적 의미: 프로세스 내에서 실행되는 여러 흐름의 단위
  • 프로세스의 특정한 수행경로
  • 프로세스가 할당받은 자원을 이용하는 실행의 단위

 

 

  • 스레드는 프로세스 내에서 각각 Stack만 따로 할당하고, Code, Data, Heap 영역은 공유
  • 스레드는 한 프로세스 내에서 동작되는 여러 실행의 흐름으로, 프로세스 내의 주소 공간이나 자원들(Heap 등)을 같은 프로세스 내에서 스레드끼리 공유하면서 실행
  • 같은 프로세스 안에 있는 여러 스레드들은 같은 Heap 메모리를 공유. 반면 프로세스는 다른 프로세스의 메모리에 직접 접근 불가
  • 한 스레드가 프로세스 자원을 변경하면, 다른 이웃 스레드(Sibling Thread)도 그 변경 결과를 즉시 볼 수 있음

 

 

자바 스레드

 

  • JVM이 운영체제의 역할. 자바에서 스레드 스케줄링은 전적으로 JVM
  • 스레드가 몇 개 존재하는지
  • 스레드로 실행되는 프로그램 코드의 메모리 위치는 어디인지
  • 스레드의 상태는 무엇인지 스레드
  • 우선순위는 얼마인지
  • 자바에는 프로세스가 존재하지 않고, 스레드만 존재. 자바 스레드는 JVM에 의해 스케줄되는 실행단위 코드 블럭

 

 

멀티 프로세스와 멀티 스레드

 

  • 멀티 프로세스: 하나의 응용 프로그램을 여러 개의 프로세스로 구성하여 각 프로세스가 하나의 작업을 처리
  • 여러 개의 자식 프로세스 중 하나에 문제가 발생하면 그 자식 프로세스만 죽는 것 이상으로 다른 영향이 확산되지 않는다
  • Context Switching에서의 오버헤드, 복잡한 통신 기법(IPC)
  • 멀티 스레드: 하나의 응용 프로그램을 여러 개의 스레드로 구성하고 각 스레드로 하여금 하나의 작업을 처리
  • 시스템 자원 소모 감소, 처리량 증가, 간단한 통신 방법으로 인한 프로그램 응답 시간 단축
  • 주의 깊은 설계 필요, 디버깅 어려움, 자원 공유의 문제 발생(동기화 문제), 하나의 스레드에 문제가 발생하면 전체 프로세스에 영향

 

 

 

Android Thread

 

안드로이드의 스레드 또한 자바 SDK에 포함된 API를 사용한다. Thread 클래스를 사용하여 새로운 스레드를 생성하고 실행하는 방법은 크게 두 가지가 있다. 하나는 Thread 클래스를 상속(extends)한 서브 클래스를 만든 다음, run() 메소드를 override하는 것이고, 다른 하나는 Runnable 인터페이스를 implements한 클래스를 선언한 다음 run() 메소드를 작성하는 것이다.

 

 

Thread와 Runnable

 

코드의 실행과 성능은 동일하다. 다만, 구현 과정이 차이나 날뿐이다. 객체지향 프로그래밍에서 클래스를 사용한다는 것은 부모 클래스의 특징을 물려받아 재사용하는 것이 기본이고, 부모의 기능을 override하거나 새로운 기능을 추가하여 클래스를 확장한다는 것을 의미한다. 이 말은 굳이 run() 메소드만 사용한다면 상속을 하지 않아도 된다.

 

코드 implements Runnable extends Thread
범위 단순히 run() 메서드만 구현하는 경우 Thread 클래스의 기능 확장이 필요한 경우
설계 논리적으로 분리된 태스크(Task) 설계에 장점

태스크(Task)의 세부적인 기능 수정 및 추가에 장점

상속 Runnable 인터페이스에 대한 구현이 간결 Thread 클래스 상속에 따른 오버헤드

 

 

Thread Pool

 

우리는 Thread를 통해서 여러가지 작업들을 병렬로 처리할 수 있다. 하지만 Thread가 증가한다고해서 처리해야할 작업들이 빠르게 처리되지 않을 수 있다. Thread 수가 증가되면 그에 따른 스케줄링으로 인해 CPU가 바빠지고 메모리 사용량이 늘어나게 되어 애플리케이션의 성능저하될 수도 있다.

 

갑작스러운 병렬작업의 극대화로 인한 무분별한 Thread 생성을 제한하기 위해서 Thread Pool(스레드 풀)을 사용한다.

 

Thread Pool은 작업 처리에 사용되는 Thread를 제한된 개수만큼 정해 놓고 작업 Queue에 들어오는 작업들을 하나씩 Thread가 맡아 처리하게 하는 것이다. 예를 들면, 애플리케이션의 한 화면에 이미지 10개가 존재한다고 가정한다면 10개의 Thread를 생성하여 동시에 이미지를 로딩할 수 있지만, Thread의 제한을 3개로 하여 순차적으로 이미지를 로딩하는 것이다.

 

안드로이드에서는 java.util.concurrent package에서 ExecutorService 인터페이스와 Executors 클래스를 제공하고 있다. Executor의 다양한 정적 메소드를 통해 ExecutorService 구현 객체를 만들어서 사용할 수 있으며 이것이 바로 Thread Pool이다.

 

ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); // Integer.MAX_VALUE
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);  // nThreads

ExecutorService fixedThreadPool2 = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors() // nCPUs
);

 

newCachedThreadPool() 메소드로 생성된 스레드 풀의 특징은 초기 Thread 수와 Core Thread 수는 0개이고, Thread 개수보다 작업 개수가 많으면 새 Thread를 생성시켜 작업을 처리한다. 1개 이상의 Thread가 추가되었을 경우 60초 동안 추가된 Thread가 아무 작업을 하지 않으면 추가된 Thread를 종료하고 Pool에서 제거된다.

 

newFixedThreadPool(int nThreads) 메소드로 생성된 Thread Pool의 초기 Thread 개수는 0개이고, Core Thread 수는 nThreads입니다. 스레드 개수보다 작업 개수가 많으면 새 스레드를 생성시키고 작업을 처리한다. 그리고 CPU Core의 수만큼 최대 Thread 를 사용하는 Thread Pool을 생성할 수 있다.

 

 

메인 스레드

 

메인 스레드의 개념은 안드로이드에서만 사용되어지는 개념이 아니다. 프로세스가 실행될 때 여러 스레드가 생성 혹은 소멸되고, 이 과정에서 스레드를 통해서 또 다른 스레드가 실행된다는 것을 알아야 한다. 즉, 프로세스가 시작될 때, 최소 하나의 스레드가 실행 중이어야만 다른 새로운 스레드를 생성할 수 있다. 이 스레드가 우리가 흔히 알고 있는 main()함수라고 일컫는, 프로세스의 시작과 동시에 실행되는 스레드 메인 스레드(Main Thread)이다. (단, 메인 스레드에서만 새로운 스레드를 생성 가능한 것은 아니다.)

 

 

안드로이드 메인 UI 스레드

 

 

 

스레드와 스레드 통신(Thread Communication)

 

프로그램에서 실행되는 모든 Thread는 기본적으로 독립적인 실행 흐름을 가진다. 일단 Thread가 실행되면 다른 Thread로부터 어떠한 간섭을 받지 않고, 다른 Thread가 어떻게 실행되고 있는지 관심을 두지 않았도 된다. 하지만, 이러한 독립성은 Thread의 본질적 특성을 얘기하는 것일 뿐, 실제 상황에서는 엔지니어들은 Thread의 실행 상태 또는 결과를 다른 Thread로 전달하도록 의도적으로 구현을 해야하는 경우가 많다. 특히 안드로이드에서는 Main Thread로 이를 전달해야하는 경우는 아주 일반적이다.

 

안드로이드 스레드 통신 - 핸들러(Handler)

 

안드로이드에서 사용할 수 있는 스레드 통신 방법은 여러 가지가 있지만, 가장 일반적으로 사용할 수 있는 방법은 핸들러(Handler)를 통해서 메세지(Message)를 전달하는 방법이다.

 

메세지(Message)

 

스레드 통신에서 핸들러를 사용하여 데이터를 보내기 위해서는 데이터 종류를 식별할 수 있는 식별자와 실질적인 데이터를 저장한 객체, 그리고 추가 정보를 전달할 객체가 필요하다. 전달할 데이터를 한곳에 저장하는 역할을 하는 클래스가 필요한데, 이 역할을 하는 클래스가 바로 Message 클래스이다.

 

메세지 풀(Message Pool)

 

일반적으로 Message가 필요할 때 새 Message 객체를 생성하면 성능 이슈가 생길 수 있으므로 안드로이드가 시스템에 미리 만들어 둔 Message Pool의 객체를 재사용하게 된다. obtain() 메소드는 빈 Message 객체를 obtain(Handler h, int what...)은 목적 handler와 다음 인자들을 담아 Message 객체를 리턴한다. 

 

메세지 큐(Message Queue)

 

Message Queue는 이름 그대로 Message 객체를 Queue 형태로 관리하는 자료구조이다. Queue라는 이름답게 FIFO(First In First Out) 방식으로 동작하기 때문에 메세지는 큐에 들어온 순서에 따라 차례대로 저장된다. 그리고 가장 먼저 들어온 Message 객체부터 순서대로 처리한다.

 

루퍼(Looper)

 

Message Queue는 Message 객체 리스트를 관리하는 클래스일 뿐, Queue에 쌓인 메세지 처리를 위한 Handler를 실행시키지는 않는다. Message Loop 즉, Message Queue로부터 Message를 꺼낸 다음, 해당 Message와 연결된 Handler을 호출하는 역할은 Lopper가 담당한다. 루퍼라는 이름에서 알수 있듯이, 메세지 처리를 위한 반복된 작업을 실행하는 것이다.

 

핸들러(Handler)

 

Handler는 Thread의 Loop와 연결된 Message Queue로 Message를 보내고 처리할 수 있게 만들어 준다. Main Thread의 메세지 처리 흐름에서 메세지 전달과 처리를 위해 엔지니어들이 접근 할 수 있는 창구 역할을 수행한다고 볼 수 있다.

 

Thread와 연관된 Handler를 얻기 위해서는 간단하게 new 키워드를 사용하여 Handler 클래스 인스턴스를 생성하기만 하면 된다. 그러면 새로운 Handler 인스턴스는 자동으로 해당 Thread와 Message Queue에 연결(Bound)되고, 그 시점부터 Handler를 통해서 Message르르 보내고 처리할 수 있게 된다. 그리고 Main Thread에 연결하고 싶다면, 인자를 통해 Lopper.getMainLooper()를 통해 이를 연결할 수 있다.

 

 

ActivityThread

 

안드로이드에서 모든 자바 애플리케이션 프로세스는 zygote 프로세스에 의해 fork된다. zygote 프로세스는 이미 초기화된 Dalvik 인스턴스이며, ZygoteInit, ZygoteConnection를 통해 새로운 자바 프로세스를 생성한다.

 

프로세스가 fork되고, ZygoteInit.main()을 통해 ActivityThread.main()을 거치면서 애플리케이션의 main() 스레드가 호출된다.

 

public final class ActivityThread extends ClientTransactionHandler {

	...
    
	public static void main(String[] args) {
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");

        // CloseGuard defaults to true and can be quite spammy.  We
        // disable it here, but selectively enable it later (via
        // StrictMode) on debug builds, but using DropBox, not logs.
        CloseGuard.setEnabled(false);

        Environment.initForCurrentUser();

        // Set the reporter for event logging in libcore
        EventLogger.setReporter(new EventLoggingReporter());

        // Make sure TrustedCertificateStore looks in the right place for CA certificates
        final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
        TrustedCertificateStore.setDefaultUserDirectory(configDir);

        Process.setArgV0("<pre-initialized>");

        Looper.prepareMainLooper(); // 메인루퍼를 준비한다.

        // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
        // It will be in the format "seq=114"
        long startSeq = 0;
        if (args != null) {
            for (int i = args.length - 1; i >= 0; --i) {
                if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
                    startSeq = Long.parseLong(
                            args[i].substring(PROC_START_SEQ_IDENT.length()));
                }
            }
        }
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop(); // 루퍼 시작

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    
    ...
    
}

 

 

 

 

 

 

참고문헌

https://developer.android.com/training/multiple-threads?hl=ko

https://gmlwjd9405.github.io/2018/09/14/process-vs-thread.html

https://magi82.github.io/process-thread/