안드로이드 스레드(Android Thread) 적용 (2)

쓰리원·2022년 6월 6일
0

Thread Usage

목록 보기
2/3
post-thumbnail

1. Thread

안드로이드 os에서 스레드는 크게 보면 2가지로 나뉘게 됩니다. 하나는 메인 스레드 그리고 작업 스레드 입니다. 메인 스레드는 UI 관련되서 이벤트를 실행하는 것을 담당하는 스레드이고, 그외의 목적을 가지는 스레드를 작업 스레드라고 합니다.

위의 그림과 같이 메인 스레드는 메세지큐에 요청들을 스택으로 쌓아놓고 순서대로 처리합니다.

Btn.setOnClickListener {
	for(i in 1..100000000L) {
        number.text = "${i}번째"
   }
}

그래서 메인 스레드에 위 코드와 같은 과도한 부하를 줄 수 있는 상황을 주게 되면, 에뮬레이터가 ANR이 되게 됩니다. 그래서 위와 같은 상황을 피하기 위해서 우리는 메인 스레드 작업을 다른 스레드로 나눠서 따로 처리를 해야하는 것 입니다.

class MainActivity : AppCompatActivity() {

    private lateinit var mainText : TextView
    private lateinit var backText : TextView
    private lateinit var startButton : Button

    private var backValue = 0
    private var mainValue = 0

    private lateinit var myThread: MyThread
    private val TAG: String = "로그"

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        startButton = findViewById(R.id.start_button)
        mainText = findViewById(R.id.mainText)
        backText = findViewById(R.id.backText)

        myThread = MyThread()
        myThread.start()

        startButton.setOnClickListener {
            mainValue++;
            mainText.setText("메인스레드 값: " + mainValue)
            backText.setText("작업스레드 값: " + backValue)
        }
    }

    inner class MyThread: Thread() {
        override fun run() {
            for(i in 1..100000000L) {
                backValue++
                Log.d(TAG, "MyThread - run: i : $i ");
                SystemClock.sleep(1000)
            }
        }
    }
}

위와 같이 데몬스레드를 활용하여 작업을 따로 분리해서 진행하면, 메인 스레드는 방해 받는 것 없이 text 값을 변화시켜줄 수 있습니다. 그런데 만약 myText.text="world hello" 부분을 myThread의 run 메서드 안에 넣으면 ANR이 발생합니다.

Main Thread에서는 UI 관련된 작업들도 처리하는데, Main Thread 이외의 일반 Thread에서는 UI 작업을 처리할 수 없도록 하였습니다. 예를 들어 EditText의 값들을 Thread1에서 변경 후 내용을 읽을 때 Thread2에서 다른 내용으로 변경을 하면 기대한 결과가 나타날 수 없고 경쟁상태(Race condition)가 되기 때문에 안드로이드 시스템에서는 Main Thread에서만 UI를 변경할 수 있도록 하였습니다.

2. Handler (핸들러)

UI 스레드와 작업 스레드를 나누는 것, 그리고 핸들러는 안드로이드에만 있는 개념입니다. 핸들러를 통해서 작업 스레드에서 UI 작업을 할 수 있게 됩니다.

class MainActivity : AppCompatActivity() {

    private lateinit var mainText : TextView
    private lateinit var startButton: Button
    private lateinit var stopButton: Button

    private lateinit var myThread: MyThread
    private lateinit var myHandler: MyHandler

    private var isRunning: Boolean = false

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        startButton = findViewById(R.id.start_button)
        stopButton = findViewById(R.id.stop_button)
        mainText = findViewById(R.id.mainText)

        isRunning = true

        myThread = MyThread()
        myThread.start()
        myHandler = MyHandler()

        startButton.setOnClickListener {
            isRunning = true
        }

        stopButton.setOnClickListener {
            isRunning = false
        }
    }

    inner class MyThread: Thread() {
        override fun run() {
            while(isRunning) {
                SystemClock.sleep(1000)
                myHandler.sendEmptyMessage(0)
            }
        }
    }

    inner class MyHandler: Handler() {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            var now = System.currentTimeMillis()
            mainText.text = "메인스레드 값 : $now"
        }
    }
}

위 코드의 isRunning으로 스레드의 실행여부를 정하게 됩니다. 그리고 그 데이터 변경을 startButton과 stopButton의 클릭 리스너에 등록하여 변경해줄 수 있습니다. startButton을 누르면 스레드를 실행할 것이고, stopButton을 누르면 스레드의 실행을 멈추게 됩니다.

inner class MyThread: Thread() {
        override fun run() {
            while(isRunning) {
                SystemClock.sleep(1000)
                myHandler.sendEmptyMessage(0)
          }
     }
}

isRunning이 true이면 즉 startButton을 누르면 현재 시간을 계산하고 1초의 시간을 기다렸다가 myHandler에 메시지를 보냅니다. 지금 이 sendEmptyMessage는 스레드에서 핸들러를 깨우는 역할만 하게 됩니다.

 inner class MyHandler: Handler() {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            Log.d(TAG, "MyHandler - handleMessage: msg : $msg ");
            var now = System.currentTimeMillis()
            myText.text = "now : $now"
        }
    }

핸들러는 스레드에서 메시지를 받아 UI 작업을 하게 됩니다. 빌드를 하고 에뮬레이터에서 버튼을 누르면 이제 1초마다 text가 변하는 것을 확인할 수 있습니다. 코드를 조금 더 개선한다면 핸들러는 now 값을 계산하는 연산이 아닌 UI 작업만 하도록 분리할 수 있습니다.

    inner class MyThread: Thread() {
        override fun run() {
            while(isRunning){
                var now = System.currentTimeMillis()
                Log.d(TAG, "MyThread - run: now : $now ");
                SystemClock.sleep(1000)
                var msg = Message()
                msg.what = 0
                msg.obj = now
                myHandler.sendMessage(msg)
            }
        }
    }

    inner class MyHandler: Handler() {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            myText.text = "now : ${msg.obj}"
        }
    }

Message 객체로 msg 변수를 생성하고 안에 .what과 .obj를 정해줍니다. obj는 핸들러에 보내고 싶은 데이터로, 연산된 now를 보내고 what은 일반적으로 when 문법을 통해 obj 어떻게 처리할지 나눌 때 사용합니다. 그런데 만약 startButton을 한번 더 누르면 앱이 꺼지는 것을 확인 할 수 있는데, 이는 스레드의 생명주기와 관련이 있습니다.

스타트 버튼을 누르면 myThread 객체가 생성이 된 상태라서 start 단계를 지나게 됩니다. 그런데 동일 쓰레드에 다시 버튼을 누르면 start 메서드를 또 호출하는 모순적인 상황이 발생하게 됩니다.

startButton.setOnClickListener {
      myThread = MyThread()
      myThread.start()
}

새로운 쓰레드를 생성하고 호출하면 새롭게 쓰레드의 생명주기가 시작하게 되서 문제가 사라집니다.

3. reference

명품 JAVA 프로그래밍 (황기태, 김효수 저)

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글