Thread 간의 통신과정 알아보기
저번 글에서 개념적인 내용에 대해 알아보았습니다, 이번 글에서는 실제로 스레드가 어떻게 통신하는지 그 과정에 대해 알아보겠습니다!
Thread 가 주고받는 Message, Runnable
저번 글에서 말했던 Handler와 Looper가 동작하는 방식에 대해 알아보기 전에 Thread가 주고받는 값에 대해 알아보려 합니다.
Thread는 Message와 Runnable이라는 객체를 주고받습니다.
Message 객체
public final class Message implements Parcelable {
public int what;
public int arg1;
public int arg2;
public Object obj;
public Messenger replyTo;
public static final int UID_NONE = -1;
public int sendingUid = UID_NONE;
public int workSourceUid = UID_NONE;
}
Message객체는 위와 같은 구조를 가지고 있습니다. 스레드끼리의 통신에서 '값'을 전달할 때 사용합니다.
Runnable 객체
public interface Runnable {
public abstract void run();
}
Runnable객체는 이름에서도 알 수 있듯, 실행 가능한 그 동작 자체를 가지고 있는 객체입니다. 주로 다른 스레드에서 수행해야 할 동작을 넘겨줄 때 사용합니다.
Thread 간의 통신 (feat. Handler, Looper)
저번 글에서 말했던것 처럼 Thread는 Handler와 Looper라는 친구들을 이용해 서로 통신합니다. 각 구성요소를 설명하며 어떻게 통신이 이루어지는지 알아보도록 하겠습니다.
Looper
Looper루퍼는 이름 그대로 계속 루프를 돌며 Looper내부의 Message Queue라는 일종의 저장소에서 FIFO형태로 데이터를 꺼내어 꺼낸 데이터가 Message 객체라면 Handler에 전달하고, Runnable 객체라면 run() 함수를 실행하여 작업을 즉시 실행합니다.
하지만 Message Queue에 쌓인 데이터가 없다면 루프는 돌지 않습니다.
또한 하나의 스레드에는 하나의 Looper만 가질 수 있습니다.
메인 스레드 또한 하나의 Looper를 가지고 있으며, Activity 시작과 동시에 Looper가 루프를 돌기 시작합니다.
*Message Queue
Message Queue는 다른 스레드에서 전달받은 Message, Runnable객체를 저장하는 Queue형태의 저장소입니다.
Handler
Handler는 전달받은 Message를 Message Queue에 넣거나, Looper로부터 전달받은 Message 객체를 처리하는 역할을 합니다.
Thread 간의 통신에서 중간다리 역할을 하는 셈이죠.
Looper의 Message Queue에 Message객체를 전달하고 싶은 경우 sendMessage() 함수를 이용, Runnable객체를 전달하고 싶은 경우 post, postDelay 등의 함수를 이용할 수 있습니다.
Looper로부터 Message객체를 전달받는 경우에도 2가지 케이스로 나눌 수 있습니다.
만약 Looper에서 꺼낸 값이 Runnable객체라면 위에서 말했듯, Handler까지 전달되지 않고 즉시 run() 함수를 이용하여 작업을 실행합니다. 하지만 Message객체인 경우에는 handleMessage콜백을 호출함으로써 Message객체를 넘겨줍니다.
백문이 불여일견, 코드로 알아보기
실제 개발하며 가장 많이 나올 것 같은 Worker Thread -> Main Thread 예제 2개 정도를 구현해보려 합니다.
case 1: Worker Thread에서 Main Thread로 Message객체 전달하기
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="텍스트"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
먼저 테스트를 위한 간단한 View를 구현하였습니다.
작업 여부를 확인하기 위한 TextView와, 작업이 실제로 Worker Thread에서 실행되는 게 맞는지 확인하기 위한 Progress Bar도 추가해 주었습니다.
Message를 전달하는 코드는 다음과 같습니다.
private val mHandler = Handler(Looper.getMainLooper()) { message ->
textView.text = message.data.getString("result")
true // 통신이 정상적으로 완료됨을 의미함.
}
먼저 메인 스레드의 Handler를 불러와주고, Message를 콜백을 통해 받았을 때 (handleMessage) 수행할 작업을 정의합니다.
저는 콜백을 받았을 때 데이터 안에 있는 result값을 꺼내 해당 값을 textView에 반영하도록 작업하였습니다.
thread {
Thread.sleep(5000L) // 5초가 걸리는 작업
val message = Message.obtain() // obtain 사용시 재사용 가능한 객체가 있다면 재사용함.
val bundle = Bundle()
bundle.putString("result", "변경된 텍스트 결과물")
message.data = bundle
mHandler.sendMessage(message)
}
그다음 thread블록을 열어 Worker스레드를 생성해 주고, 작업 후 결과물을 Message에 담에 sendMessage를 통해 전달합니다.
그럼 아래 영상과 같이 작업 중에 UI가 중지되지 않고 5초 뒤 결과 텍스트가 반영되는 것을 확인하실 수 있습니다.
case 2: Worker Thread에서 Main Thread로 Runnable객체 전달하기
이번엔 Worker Thread에서 Main Thread로 Runnable객체를 전달해보려 합니다.
예제는 case 1과 똑같은 구조를 사용하고 스레드 코드만 Message를 넘겨 텍스트를 변경하는 게 아닌, post를 통해 텍스트를 변경하는 작업 자체를 넘겨보도록 하겠습니다.
먼저 case 1과 똑같이 Handler를 선언해 줍니다.
private val mHandler = Handler(Looper.getMainLooper())
그다음 똑같이 thread블록을 열어 작업을 실행합니다.
thread {
Thread.sleep(5000L) // 5초가 걸리는 작업
mHandler.post {
textView.text = "변경된 텍스트 결과물"
}
}
이 코드 또한 위 예제와 동일한 결과물이 나오는 것을 확인할 수 있습니다.
추가로 post 이외에도 좀 더 간편하게 쓸 수 있는 runOnUiThread라는 함수가 있습니다.
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
해당 코드는 현재 thread를 판별하여 main이라면 인자로 받은 Runnable을 바로 실행하고, main이 아니라면 예제와 같이 post를 이용하여 main으로 Runnable객체를 전송하는 코드입니다.
별도로 Handler를 선언할 필요가 없어 간단하게 이용하기 좋은 기능인 것 같습니다.
마무리
오늘은 Android환경에서 Thread가 어떻게 상호작용 하는지에 대해 알아보았습니다. 글 읽어주셔서 감사합니다!
( 글에 대한 피드백은 언제나 환영입니다! )
reference
https://hungseong.tistory.com/26
https://velog.io/@haero_kim/Android-Looper-Handler-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90
'Android' 카테고리의 다른 글
[Android/Basic] 3. Activity의 State Changes 알아보기 (0) | 2024.08.21 |
---|---|
[Android/Basic] 2. Activity의 LifeCycle 알아보기 (3) | 2024.07.22 |
[Android/Basic] 1. Android 4대 컴포넌트 알아보기 (1) | 2024.07.22 |
[Android/Thread] 1. Android의 Thread환경 (0) | 2024.05.16 |
[Android] Firebase Remote Config 알아보기! (3) | 2024.03.14 |