동기와 비동기(Thread & Handler)

💡동기, 비동기의 차이에 대해서 이해하고 안드로이드에서 Thread와 Handler가 어떻게 동작하는지 정리하였습니다.

 

Synchronous vs Asynchronous

  • 동기와 비동기는 작업의 처리 방식을 나타내는 개념으로, 작업의 실행 순서와 대기 방식에서 차이가 있습니다.

동기(Synchronous)

  • 동기적 처리는 작업이 직렬적으로 처리되며, 하나의 작업이 끝날 때까지 다음 작업을 기다리는 방식입니다.
  • 현재 실행 중인 작업이 완료될 때까지 다른 작업을 수행할 수 없으며, 결과가 즉시 반환됩니다.
  • 호출한 코드가 끝날 때까지 대기 상태가 되며, 순차적인 처리로 로직이 단순합니다.
fun synchronousExample() {
    println("작업 시작")
    
    // 동기적으로 데이터를 가져옴 (예: 파일 읽기)
    val data = fetchDataSynchronously()
    
    println("데이터 처리 중: $data")
    println("작업 종료")
}

fun fetchDataSynchronously(): String {
    Thread.sleep(2000) // 2초 대기
    return "동기 데이터"
}

비동기(Asynchronous)

  • 비동기적 처리는 여러 작업을 병렬로 처리할 수 있으며, 하나의 작업이 완료될 때까지 다른 작업을 중단하지 않고 계속해서 실행합니다.
  • 결과를 기다리지 않고 작업을 수행하며, 작업이 완료되면 콜백이나 이벤트로 결과가 반환됩니다.
fun asynchronousExample() {
    println("작업 시작")
    
    // 비동기적으로 데이터를 가져옴 (예: 네트워크 요청)
    fetchDataAsynchronously { data ->
        println("데이터 처리 중: $data")
    }
    
    println("작업 종료")
}

fun fetchDataAsynchronously(callback: (String) -> Unit) {
    Thread {
        Thread.sleep(2000) // 2초 대기
        callback("비동기 데이터")
    }.start()
}

안드로이드에서는?

  • 동기적 처리는 SQLite와 같은 로컬 데이터베이스를 저장하거나 읽어오는 작업을 수행합니다.
  • 하지만 이를 메인 스레드에서 작업하면 앱이 멈추는 문제가 발생할 수도 있습니다.
  • 비동기 처리는 네트워크 요청이나 이미지 로딩 같은 작업에서 수행하며, Coroutines, RxJava 등을 사용할 수 있습니다.
  • ViewModel에서 LIveData나 Flow와 Coroutine을 결합하여 비동기 데이터를 UI에 반환하는 방식을 주로 활용합니다.

Block vs UnBlock

  • Block은 작업이 완료될 때까지 현재 스레드나 프로세스가 대기하는 방식입니다.
  • 다른 작업이 중단되고 해당 작업이 완료될때까지 기다리며, Synchronous와 비슷한 형태라고 볼 수 있습니다.
  • UnBlock은 작업이 진행 중이더라도 해당 작업의 결과를 기다리지 않고 다른 작업을 계속 진행합니다.
  • 작업이 완료되면 알림을 받거나 결과를 확인하는데, Asynchronous와 비슷한 형태라고 볼 수 있습니다.

Main Thread

  • Android Compoonents들이 동작하고 애플리케이션에서 실행 중인 다른 Component가 없으면 Android System은 단일 Thread Process로 동작합니다.
  • 대부분의 Components는 같은 Process와 Thread에서 실행되며 이를 Main Thread라고 부릅니다.

Main Thread는 화면을 그리는 기능도 담당하기 때문에 UI Thread라고 불리며 Android UI Kit의 구성요소(android.widget, android.view)와 상호작용하고 UI의 Event를 사용자에게 응답하는 역할을 수행합니다.

Main Thread와 비동기

  • 안드로이드는 Main Thread만을 가지는 Single Thread System으로 활용되며, 사용자의 상호작용으로 Resource를 많이 소모하는 작업을 수행할 수 있습니다.
  • Main Thread에서 이러한 작업을 진행하면 ANR 에러가 발생할 수 있으며, 사용자에게 낮은 성능과 심각한 에러로 이어질 수 있습니다.
  • [ANR] : Application Not Response의 약자로 Main Thread를 5초간 차단하는 경우 발생
  • 이를 해결하기 위해서 다른 Thread에서 비동기 작업을 수행하며, Android UI Kit 관련 작업은 Main Thread에서 동작하도록 하는 것이 일반적인 방식입니다.

Main Thread는 차단되면 안되며, 다른 Thread에서는 UI 작업이 아닌 Resource를 많이 소모하는 작업을 수행해야 합니다. 이러한 작업을 Background, Worker Thread에서 수행한다고 할 수 있습니다.

Background Thread

  • Work Thread라고 불리며, Main Thread를 차단할 수 있는 무거운 작업을 수행합니다.
  • 경우에 따라서 Background에서 UI 업데이트를 수행할 경우의 메서드를 안드로이드에서는 제공하고 있습니다.
Activity.runOnUiThread()
View.post(Runnable)
View.postDelayed(Runnable, long)
  • 위 코드를 사용하면 항상 Main Thread에서 동작하도록 할 수 있으며, Thread로부터 안전하다고 할 수 있습니다.
    • Thread Safe

Looper와 Handler

  • 안드로이드에서 스레드 간 통신을 처리하는 중요한 요소입니다.
  • UI 스레드와 백그라운드 스레드 간의 작업 전달과 메시지 처리를 담당합니다.

Message Queue

  • 스레드 자신이나 다른 스레드로부터 전달 받은 Message와 Runnable을 보관하여 FIFO 방식으로 처리합니다.

Looper

  • Looper는 스레드의 메시지 큐를 관리하면서 메시지 또는 작업을 차례대로 처리하는 역할을 합니다.
  • 안드로이드의 메인 스레드는 기본적으로 Looper를 가지고 있어서, UI 업데이트와 작업 실행을 순차적으로 처리합니다.
  • 메시지 큐에서 메시지와 작업을 계속해서 반복 처리하는 루프를 생성합니다.
  • 다른 스레드에서도 Looper를 설정하여 메시지를 처리할 수 있습니다.
class ExampleThread : Thread() {
    override fun run() {
        // Looper 준비
        Looper.prepare()

        // 이 스레드는 루퍼로 계속해서 메시지 큐를 처리하게 됨
        Looper.loop()
    }
}

Handler

  • 스레드 간에 메시지와 작업을 전달하는 도구입니다.
  • 주로 Ui 스레드와 백그라운드 스레드 간의 통신을 돕기 위해 사용됩니다.
  • Handler는 특정 Looper에 연결되어 메시지 큐에 작업을 넣거나 메시지를 보내고, 그 메시지가 처리되었을 때 콜백을 통해 특정 작업을 수행할 수 있습니다.
  • 백그라운드 스레드에서 UI 스레드로 작업 결과를 전달할 때 주로 수행하며, 특정 지연 시간 후 작업을 실행하는 경우에도 활용됩니다.
class ExampleHandler(looper: Looper) : Handler(looper) {
    override fun handleMessage(msg: Message) {
        // 메시지 처리
        when (msg.what) {
            1 -> Log.d("HandlerExample", "메시지 1 처리")
            2 -> Log.d("HandlerExample", "메시지 2 처리")
        }
    }
}
  • handleMessage()를 통해 Message를 처리하며, Handler에 전달 된 메시지는 이 메서드를 통해 처리됩니다.
// Handler 사용 예시
val handler = Handler(Looper.getMainLooper())  // 메인 스레드에서 실행될 핸들러

// 3초 후 실행될 작업
handler.postDelayed({
    Log.d("HandlerExample", "3초 후 실행된 작업")
}, 3000)

Looper와 Handler의 상호작용

  • Looper는 스레드가 무한 루프를 돌며 메시지 큐에 담긴 작업을 처리할 수 있도록 해줍니다.
  • Handler는 특정 스레드에 작업을 전달하는 중개자 역할을 합니다.
    • 메시지나 Runnable을 메시지 큐에 추가하고, 이 큐는 Looper를 통해 순차적으로 처리됩니다.

스레드가 Looper를 준비하면, 무한 루프가 돌며 메시지 큐에 있는 작업을 처리할 준비를 합니다. Handler가 메시지 또는 작업을 메시지 큐에 추가하고, Looper가 이 메시지나 작업을 처리하여 Handler에 전달하여 처리 결과를 얻습니다.

UI 업데이트

  • Android에서 UI는 메인 스레드에서만 업데이트하며, 백그라운드 스레드에서 직접 업데이트하지 않고 Handler를 통해 메인 스레드에 작업을 전달합니다.
val handler = Handler(Looper.getMainLooper()) // 메인 스레드의 루퍼를 사용

// 백그라운드 스레드에서 실행
Thread {
    val result = doBackgroundWork() // 백그라운드 작업 수행

    // 작업 결과를 UI 스레드로 전달
    handler.post {
        // UI 업데이트
        textView.text = result
    }
}.start()

HandlerThread?

  • HandlerThread는 Looper를 내장한 스레드입니다.
  • 백그라운드에서 반복적인 작업을 처리하고, Handler를 사용하여 메시지 큐에 작업을 전달할 수 있습니다.
val handlerThread = HandlerThread("BackgroundThread")
handlerThread.start()

val backgroundHandler = Handler(handlerThread.looper)

backgroundHandler.post {
    // 백그라운드에서 실행될 작업
    Log.d("HandlerThread", "백그라운드 작업 실행 중")
}
  • 별도의 스레드에서 Looper를 생성하고, 그 스레드에서 반복적으로 작업을 처리할 수 있습니다.

참고

https://jslee-tech.tistory.com/39