⚙️ 안드로이드 Thread와 Handler 1

sery270·2021년 3월 29일
2

Android

목록 보기
4/13
post-thumbnail

안녕하세요 :) 안드로이드의 Thread와 Handler의 동작구조에 대해 이해하기위해, Thread, Looper, MessageQueue를 살펴보려고 합니다. 이 글에선 각 요소들의 Android Framework 소스를 확인하고, 그들의 동작방식을 설명합니다. 기본으로 강조되는 내용이므로, 깊은 이해를 도모하시길 바랍니다. 앗 그리고 분량이 많아 쪼개버린, Handler와 Message, Runnable을 다루는 다음 포스팅도 참고해주세요 ! 그럼 오늘도 화이팅 입니다 🌿

1️⃣ 안드로이드에서 왜 Thread-Looper-Handler 구조를 사용할까 ?


  • 안드로이드의 메인스레드는 컴포넌트 생명주기 메서드와 그 안의 메서드 호출을 기본적으로 담당하고 있습니다. 메인 스레드에 대한 더 자세한 내용은 이 게시물을 참고해보세요 !

  • 이 작업들의 경합 상태, 교착 상태를 방지하고자, 메인스레드엔 단일 스레드 모델이 적용됩니다.

    단일 스레드 모델은 자원 접근에 대한 동기화를 신경쓰지 않아도 되고, 작업전환(context switching) 비용을 요구하지 않으므로, 경합 상태와 교착 상태를 방지할 수 있다.

  • 안드로이드에서의 단일 스레드 모델이란 안드로이드 화면을 구성하는 뷰나 뷰그룹을 하나의 스레드에서만 담당하는 원칙을 말합니다. 단일 스레드 모델은 아래 두 가지 규칙을 갖습니다.

    첫째, 메인 스레드(UI 스레드)를 블럭하지 말 것

    둘째, 안드로이드 UI 툴킷은 오직 UI 스레드에서만 접근할 수 있도록 할 것

  • 단일 스레드에서의 긴 작업은 어플리케이션의 반응성을 낮추거나, ANR의 원인이 될 수 있습니다. 따라서 메인 스레드에선 정해진 최소한의 일만 담당하고, 특히 긴 작업은 다른 스레드가 담당하게 해야합니다.

  • 따라서 이 메인 스레드와 다른 스레드가 협업하기위해, 스레드간 통신이 필요하게 되었습니다.

  • 안드로이드에선 Looper와 Handler를 사용하여, 다른 스레드와 메인 스레드간의 통신을 할 수 있습니다.

  • 위 내용을 정리하면, 안드로이드에서의 Thread-Looper-Handler 구조는, 메인 스레드에 단일 스레드 모델이 적용되면서 요구되는 스레드간의 통신 방법을 지원하는 구조라고 이해하면 좋습니다.

2️⃣ Thread-Looper-Handler 구조는 어떻게 작동하는가 ?


  • 안드로이드에서 사용하는 스레드간 통신 방법은(Thread-Looper-Handler 구조는) 위 그림으로 요약할 수 있습니다.
  • 위 그림은 송신 스레드인 Thread#1수신 스레드인 Thread#2가 통신하는 그림입니다.
    • 가장 먼저, 송신 스레드(Thread#1)에서 수신 스레드(Thread#2)로, message을 send하거나 runnable을 post합니다.
    • 수신 스레드(Thread#2)의 Handler가 수신처리를 합니다. 수신처리란 수신 스레드(Thread#2)의 Handler는 송신 스레드(Thread#1)의 message, runnable을 받아 MessageQueue에 차례로 삽입하는 것을 의미합니다.
    • MesseageQueue에선 FIFO 방식으로, message, runnable이 push되고 pop됩니다.
    • 차례로 pop된 message, runnable의 작업을 수신 스레드(Thread#2)의 Handler가 처리합니다.
    • 위 과정은 Looper에 의해 MessageQueue에서 Null이 pop될때까지 반복하게됩니다.
  • 즉, 송신 스레드에서 수신 스레드에게 작업을 보내면, 수신 스레드의 Handler가 작업을 순차적으로 접수하고, 처리하는 방식으로, 여러 스레드 간의 통신과 작업이 처리되는 것입니다.
  • 여러 스레드간의 통신을 매개하고, 그 작업을 처리하는 곳이 바로 Handler라고 이해하면 쉽습니다.

Thread와 Handler, Looper, MessageQueue, Message, Runnable 의 관계

  • Thread별로 Looper를 생성합니다. Thread의 TLS에 Looper가 저장되고, 꺼내어져 사용됩니다. Looper ∈ Thread

  • Looper별로 MessageQueue를 가집니다. 즉, Looper 자신의 loop 대상이 담긴 곳(MessageQueue)이 지정되어있다고 이해하면 좋습니다. Looper# - MessageQueue#

  • MessageQueue는, 이름에서도 알 수 있듯이, Queue 자료구조입니다. Message, Runnable 객체가 FIFO 방식에 따라 들어가고 나오게 됩니다. Message, Runnable ∈ MessageQueue

  • Handler는 어디서 생성되는지, 어떤 Looper를 사용하는 지에 따라 통신 대상 스레드가 달라집니다. 메인스레드, 다른 일반스레드는 물론 자기 자신도 통신대상이 될 수 있습니다. Handler에 대한 자세한 내용은 다음 포스팅인 안드로이드 Thread와 Handler 2을 참고해보세요 !

3️⃣ Thread


Thread란 무엇일까 ?

  • 공식문서에 의하면, java.lang.Thread는 프로그램에서 실행된는 스레드를 의미하며, JVM에서 지원하는 바와 같이 여러개의 스레드를 동시에 실행할 수 있는 프로세스의 구성요소 입니다.

    A thread is a thread of execution in a program. The Java Virtual Machine allows an application to have multiple threads of execution running concurrently.

  • 스레드는 프로세스의 작업 단위로 설명되기도 합니다. 이와 관련하여 프로그램, 프로세스, 스레드, TLS 를 참고해보세요 !

Thread 객체는 어떻게 생성될까 ?

  • (위 그림 출처) 여러개의 Thread 클래스의 생성자 중, 추후에 나올 Runnable에 대한 설명을 돕기 위해, Runnable 매개변수의 유무로 생성자를 2갈래로 구분지어 설명하려 합니다.

  • 첫 번째 방식은 기본 생성자인 Thread() 생성자로 스레드를 생성하는 방식입니다. 내부적으로 run()을 Override하여 사용합니다.

  • 두 번째 방식은 Thread(Runnable runnable) 생성자로 스레드를 생성하는 방식입니다. 따로 Runnable 인터페이스를 구현한 객체를 생성하여 전달하여야합니다.

    • 공식문서에 따르면, Runnable 인터페이스를 구현한 클래스는 run()으로 정의 되어야한다고 합니다. 즉, Runnable 객체는 run()에 대한 내용을 가지고 있는 객체라 이해하시면 좋습니다.

      The Runnable interface should be implemented by any class whose instances are intended to be executed by a thread. The class must define a method of no arguments called run.

    • 매개변수로 전달된 runnable 객체는 스레드가 실행될때 사용됩니다. 즉 스레드의 start()가 호출되면, runnable의 run()이 호출됩니다.

Thread 객체는 어떻게 실행될까 ?

  • Thread에서 run()을 Override한 경우 (첫 번째 방식)

    • start()함수가 실행되면, JVM에 의해, 해당 스레드의 run()이 호출됩니다.

      Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread.

      • Thread.start() -> Thread.run()
    • new thread().start();
    • 결과적으로는 동시에 2개의 스레드가 실행됩니다.

      • the current thread : start()의 리턴 값인 Thread 객체
      • the other thread : 위 스레드의 run()이 실행되는 또다른 Thread 객체
  • Runnable 사용하는 경우 (두 번째 방식)

    • Thread의 run()은 내부적으로 Runnable의 run()을 호출합니다.

      • Thread.start() -> Thread.run() -> Runnable.run()
    • new Thread(Runnable).start();

    If this thread was constructed using a separate Runnable run object, then that Runnable object's run method is called; otherwise, this method does nothing and returns.

    Runnable: the object whose run method is invoked when this thread is started. If null, this classes run method does nothing.

Thread를 정의하여 사용하려면 어떻게 해야할까?

  • 스레드를 직접 정의하여 사용하는 방법으로는, 어떤 것이 부모가 되는지에 따라 두가지 방법으로 나뉘게 됩니다.

    • Thread 클래스를 상속받아 정의하는 방법

      • class MyThread extends Thread {}
    • Runnable 인터페이스를 구현하여 정의하는 방법

      • class PrimeRun implements Runnable {}

        A class that implements Runnable can run without subclassing Thread by instantiating a Thread instance and passing itself in as the target.

4️⃣ Looper


Looper란 무엇일까 ?

  • Looper란 Handler가 처리할 작업을 계속해서 pop 해주는 구조를 의미합니다. 즉 MesseageQueue에 있는 message, runnable을 계속해서 FIFO 방식으로 하나씩 꺼내줍니다.

  • Looper란 명칭에서 알 수 있듯이, 이렇게 Looper는 반복 작업을 맡고 있습니다.

  • Looper는 Handler와 가장 많이 소통합니다.

    Most interaction with a message loop is through the Handlerclass.

  • Looper의 반복 작업은, 스레드 종류에 상관없이 (메인스레드, 일반스레드에 상관없이) loop()함수의 무한 loop을 통해 이뤄집니다.

    • 오픈소스인 안드로이드 프레임워크단의 소스를 통해 loop() 함수의 구현을 확인해봅시다. 여기를 통해 소스 전체를 확인하실 수 있습니다.

    • loop() 함수안은 pop한 내용물이 null일때까지 반복하는 무한 loop로 구성되어있습니다. 이 무한 loop안에서, msg.target.dispatchMessage(msg); 를 통해, Handler인 target이 작업을 처리하게 만들어줍니다.

      • 여기서 dispatch란 ready status를 running status로 만들어주는 작업을 의미합니다.

        /**
         * Run the message queue in this thread. Be sure to call
         * {@link #quit()} to end the loop.
         */ 
        public static void loop() {
                final Looper me = myLooper();        
           // 생략
                final MessageQueue queue = me.mQueue;
           // 생략
                for (;;) { // 무한 루프 시작 ‼️
                    Message msg = queue.next(); // might block
                    if (msg == null) {
                        // No message indicates that the message queue is quitting.
                        return;
                    }
                  	// 생략
                    try {
                        msg.target.dispatchMessage(msg);
                        if (observer != null) {
                            observer.messageDispatched(token, msg);
                        }
                        dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
                    } catch (Exception exception) {
                        if (observer != null) {
                            observer.dispatchingThrewException(token, msg, exception);
                        }
                        throw exception;
                    } finally {
                        ThreadLocalWorkSource.restore(origWorkSource);
                        if (traceTag != 0) {
                            Trace.traceEnd(traceTag);
                        }
                    }
           					// 생략
                    msg.recycleUnchecked();
                } // 무한 루프 끝 ‼️
            }

Looper는 어떻게 생성될까 ?

  • 기본적으로 스레드 상관없이, 각 Looper는 자신을 생성한 스레드의 Thread Local Storage (TLS)에 저장됩니다.

  • 메인스레드의 Looper인 MainLooper는, API 30 이전엔, ActivityThread.java의 main 함수에서 prepareMainLooper() 호출을 통해 MainLooper생성되고 loop() 호출에 의해 MainLooper동작되었습니다.

    // ActivityThread.java
    
    public static void main(String[] args) {
      // 생략
      Looper.prepareMainLooper();
      // 생략
      Looper.loop();
    	// 생략
    } 
  • 하지만 API 30 이후, prepareMainLooper()가 deprecated 되었는데, 안드로이드 환경에서 MainLooper를 직접 생성해주는 방식으로 바뀌었기 때문입니다. 따라서 개발자들은 (원래도 그랬지만) MainLooper의 생성에 관여하지 않고, getMainLooper()를 통해 가져다 쓰기만 하면 됩니다.

    * @deprecated The main looper for your application is created by the Android environment, * so you should never need to call this function yourself.

  • 반면, 일반스레드기본적으로 Looper를 가지고 있지 않아, 각 스레드의 Handler를 사용하기 위해선 직접 Looper를 만들어주어야 합니다.

    Threads by default do not have a message loop associated with them.

    • 일반스레드가 Looper를 가지지 않는 이유를 첨언하자면, Looper란 안드로이드에서 도입한 구조인데, 안드로이드의 일반스레드는 Java의 스레드를 사용하기 때문에 기본적으로 Looper를 가지지 않습니다.
  • 일반스레드의 Looper를 생성하는 방법은 prepare() 함수를 사용하는 것 입니다. 또한 메인스레드와 마찬가지로, loop() 호출을 통해 동작하게 됩니다. 정리하면, 개발자는 일반스레드를 사용하는 곳에서 prepare() 호출을 통해 Looper생성하고 loop() 호출에 의해 Looper동작하게 합니다.

    // Android Developers : Looper   
    
    class LooperThread extends Thread {
          public Handler mHandler;
    
          public void run() {
              Looper.prepare();
    
              mHandler = new Handler(Looper.myLooper()) {
                  public void handleMessage(Message msg) {
                      // process incoming messages here
                  }
              };
    
              Looper.loop();
          }
      }

5️⃣ MessageQueue


  • MessageQueue는 Message를 담는 자료구조로, 개수 제한과 중간 삽입 속도이 쉬운 링크구조의 Queue로 구현되어있습니다.

MessageQueue의 Queueing

  • -delayed()가 Queueing에 미치는 영향에 대해 이야기해보겠습니다. 만약 현재 시각이 10이고, handler.postDelayed(runnable, 200); 이 호출되었다면, 해당 runnable이 MessageQueue에 삽입되고 실행되는 시각은 언제일까요 ?

    • enqueue > 먼저 삽입되고 200 delay될까요 ? 200 delay후 삽입될까요 ?

    • dispatch > dispatch되는 순서는 어떻게 될까요 ?

    • 정답은 바로 -delayed가 호출된 10에 enqueue되며, 현재시간(uptimeMillies, 10) + delayMilillis(200) 의 시간 이후로, 즉 200에 (또는 200 이후에) 해당 작업이 dispatch 되도록 합니다.

      • -delayed는 내부적으로 -AtTime을 호출합니다. 즉 -delayed의 현재시간(uptimeMillies) + delayMilillis이, -AtTime의 매개변수인 uptimeMillies에 들어가게 되는 것 입니다.
      • 호출된 handler.postDelayed(runnable, 200)는 내부적으로 handler.postAtTime(runnable, 210)를 호출합니다.
    • 내부적으론, MessageQueue의 msg, runnable의 정렬과 딥 슬립을 통해 특정 시간(uptimeMillies, 타임스탬프) 이후로, 해당 작업이 dispatch 되도록합니다.

    • MessageQueue가 링크구조를 사용하는 이유는, 타임스탬프를 기준으로, 정렬을 수행하는 것, 중간에 삽입하는 것 과도 관련이있습니다.

      MessageQueue에는 Message가 실행 타임스탬스순으로 삽입되고 링크로 연결되어, 실행 시간이 빠른 것부터 순차적으로 꺼내어진다.

      post(), send() 메서드에서 실행 시간이 전달되고, 나중에 호출한 것이라도 타임스탬프가 앞서면 큐 중간에 삽입된다. 이것이 삽입이 쉬는 링크 구조를 사용한 이유다.

#️⃣ Reference


profile
개발세리의 성장기🌿

0개의 댓글