Thread(스레드)

Thread(스레드)를 여러 개 수행하도록 만드는 코드를 만들기 전에 이론적인 부분을 공부하고 넘어가자.

Thread : 동시에 여러 작업을 수행하기 위해 사용되는 개념.
우리 CPU는 한개의 작업만 가능하다. 동시 작업을 위해서 CPU를 가상의 여러 작은 부분으로 나누고 각 프로세서가 다른 작업을 수행함으로써 동시 작업을 할 수 있는데 이를 스레드/ 멀티스레드/ 경량 프로세스라고 한다.

  • 함수 : 하나의 작업이 끝나야 다음 작업이 진행된다.
  • 스레드 : 하나의 작업이 끝나기 전에 다른 작업을 동시에 진행시킬 수 있다.

Program(프로그램) & Process(프로세스)

  • Program : 컴퓨터에 의해 실행되는 명령과 데이터 집합
    저장장치에 대부분 파일형태로 저장됨. CPU에 의해 메모리로 로딩되고 실행된다.

OS(운영체제:Operating System)의 발전으로 기존에 프로그램이 컴퓨터 하드웨어에 의해 직접 실행되던 구조에서 OS가 프로그램의 실행을 담당하는 구조로 변했다. 즉, CPU나 메모리에 대한 관리를 담당하는 OS가 프로그램의 실행을 관리 -> 저장장치에 있는 프로그램을 메모리에서실행하고 리소스 할당과 관리를 OS가 담당하게 되었다.
이는 시스템 효율 측면에서 매우 큰 장점 : 여러 프로그램을 동시에 실행 가능 + 하나의 프로그램을 동시에 여러개 실행 가능.
- 프로세스 : 현재 메모리에 로딩되고 실행 중인 프로그램

Process & Thread

OS에 의해 프로그램이 메모리에 로드되고 프로세스가 실행되면 프로세스는 순차적으로 실행한다. (함수처럼) 그런데 어떤 경우에는 main() 함수에서 시작되는 하나의 코드 실행 흐름만으로는 처리하기 힘든 동작을 구현해야 할 수도 있다.
프로세스에서 실행되고 있는 하나의 실행 흐름과는 다른 독립적인 실행흐름을 만들면 어떨까?

Thread : 프로세스 내에서 실행되는 각각의 '독립적인 실행 흐름'. 하나의 프로세스 내에서 두개 이상의 스레드가 동작하도록 프로그래밍하는 것을 멀티스레드 프로그래밍이라고 한다. 흔히 스레드에 대해서 이야기할 때 왜 사람의 팔에 비유했는지 이해가 될 것이다.

Main Thread

스레드는 프로세스 실행 중 언제든지 필요에 따라 실행되고 중단될 수 있는데 이는 기존에 이미 실행 중인 스레드에 의해서 수행될 수 있다. 즉, 최소 하나의 스레드가 실행 중이어야만 다른 새로운 스레드를 만들 수 있다.

  • Main Thread(메인 스레드) : 프로세스의 시작과 동시에 처음으로 실행되는 스레드

하지만 새로운 스레드가 오직 메인 스레드에 의해서만 실행되는 것은 아니다.

동기화와 스레드 간섭

필요에 따라 스레드들을 여러 개 만들었다고 하자. 그런데 여러 스레드들이 충돌이 일어나 원하지 않는 결과가 나오거나 모듈의 순서가 꼬일 수 있다.

  • 동기화 : 여러 스레드가 같은 프로세스 안에서 자원을 공유하기 때문에 영향을 주는 것.
    만약 A 스레드가 처리하고 있떤 내용을 중간에 다른 스레드가 처리하게 되면 충돌이 발생하거나 원치 않은 결과를 받을 수 있다. => 스레드 간섭


위 그림에서는 TextView에 메인스레드와 서브스레드가 같이 접근하고 있다.
안드로이드에서는 동기화 문제 때문에 워커스레드의 모든 UI 작업은 핸들러를 통해 메인 스레드로 보내진다.

  • Main Thread
    안드로이드의 직접적인 UI 작업(화면 구성에 관한 기능)을 하는 스레드
    따라서 오래 걸리는 작업은 워커스레드에서 하도록 지정해야 한다.
  • Worker Thread
    UI 작업을 하지 않은 스레드. 보통 시간이 오래 걸리는 작업을 함..

Thread 사용하기

안드로이드에서의 스레드는 역시 자바 SDK에 포함된 API를 사용한다. Thread 클래스를 사용하여 새로운 스레드를 만들고 실행하는 방법은 크게 두가지

  • Thread 클래스를 상속한 서브 클래스를 만든 후 Thread 클래스의 run() 메소드를 override
  • Runnable 인터페이스를 구현한 클래스를 선언한 후 run() 메서드 작성

이에 대해서는 아래에서 구체적으로 알아보도록 하자

Thread 클래스 상속(extends)

    1. Thread 클래스를 상속한 서브 클래스를 만들기
    1. Thread 클래스의 run() 메소드를 override
    1. 클래스 인스턴스를 생성하고 start() 메서드를 호출.

class NewThread : Thread() {
    override fun run() {
    	// TODO : thread running codes.
    }
}    
newthread = NewThread()
newthread.start()

코틀린의 thread()

코틀린에서는 조금 더 간편하게 Thread()를 사용할 수 있다. thread() 안에 인자로 start = true 를 전달하면 블럭 안의 코드를 실행할 수 있다.

class MainActivity : AppCompatActivity() {
	override fun onCreate(savedInstanceState : Bundle?){
	super.onCreate(savedInstanceState)
	    ...
   	thread(start = true) {	//'kotlin.concurrent.thread' 를 import 해야 한다.
   	...
   	}
    }
}

Thread를 상속, run() 메서드 override 구현, start() 메서드 호출. 이 간단한 코드만으로 새로운 스레드를 만들고 실행할 수 있다.

Runnable 인터페이스 구현 (implements)

Runnable 인터페이스를 구현하는 법도 Thread를 클래스를 상속하는 법만큼 간단하다.

    1. Runnable 인터페이스를 구현하는 클래스 선언
    1. run 메서드 override
    1. 자식 Runnable 클래스 인스턴스 생성 및 Thread 클래스 인스턴스 생성
    1. start() 메서드 호출
class NewRunnable : Runnable {
	override fun run() {
    
    }
}

val runnable = NewRunnable()
val newThread:Thread = Thread(runnable)
newThread.start()

Runnable을 구현하여 스레드를 만드는 법보다 Thread 클래스를 상속받아 스레드를 만드는 것이 아주 조금 더 할일이 많아 보인다. Runnable 인터페이스를 구현한 것에 더해 Thread 클래스의 인스턴스를 생성한 후 start() 메서드를 호출해야 한다.

그렇다면 두 가지 방법의 차이와 어떤 상황에서 어떤 방법을 선택해야 할지 알아봅시다.

Thread VS Runnable

먼저 위의 두 방법은 override run() 메서드 코드의 실행과 성능이 동일히다. 단지 구현 과정이 차이가 날 뿐이다.

객체지향프로그래밍에서 클래스를 상속받는다는 것은 부모 클래스의 특징을 받아 재사용하는 것. 부모의 기능을 override하거나 새로운 기능을 추가하여 클래스를 확장할 수 있다. 즉, 클래스의 기능을 override하거나 확장할 필요가 없다면 굳이 클래스를 상속받지 않고 기존 클래스를 그대로 사용하면 된다.

스레드를 실행하기 위해서는 Thread 클래스가 제공하는 기능을 사용해야 한다. 스레드로 실행된 메서드로 override 해야 한다. 그래서 위에서는 Thread 클래스를 상속받고 스레드 실행 메서드인 run() 메서드도 override 한 것이다.

그런데 굳이 run() 메서드 하나만을 위해 Thread 클래스를 상속받는 것은 불필요해보인다. Thread 클래스를 상속하지 않고 run() 메서드 코드만을 작성해서 Thread의 start() 메서드로 전달할 수 있으면 좋을텐데...?

Runnable 인터페이스는 추상적으로 선언된 단 하나의 메서드 run() 메서드만을 가진다. 그러므로 Runnable 인터페이스를 구현하는 클래스를 만들고 run() 메서드를 override 한 후 Thread의 생성자에 전달하고 start()메서드를 호출함으로써 스레드를 실행할 수 있다.

당연히 스레드를 만들 때 run() 메서드 외에 Thread 클래스의 기능을 override하거나 확장이 필요하다면 Runnable 인터페이스 구현이 아닌 Thread 클래스 상속 방법으로 스레드를 만들어야 한다.

항목Runnable 인터페이스 구현Thread 클래스 상속
코드implements Runnableextends Thread
범위단순히 run() 메서드만 구현Thread 클래스의 기능 확장이 필요한 경우
설계논리적으로 분리된 태스크(Task)설계에 장점태스트(Task))의 세부적인 기능 수정 및 추가에 장점
상속Runnable 인터페이스에 대한 구현이 간결Thread 클래스 상속에 따른 오버헤드

물론 개발자의 취향과 코드 작성 정책, 트렌드에 맞게 알아서 잘 딱 깔끔하게 센스하게 합시다.

runOnUiThread로 UI에 접근하기

안드로이드에서는 메인 스레드가 아닌 스레드에서는 UI에 접근할 수 없다. 즉, 워크 스레드내에서는 화면에 표시될 뷰에 직접 접근할 수 없다. 스레드에서 처리한 작업을 UI에 반영하기 위해 runOnUiThread를 사용할 수 있다.

class MainAcitivy : AppCompatActivity(){
	override fun onCreate(saveInstanceState: Bundle?){
    	super.onCreate(savedInstanceState)
        ...
        thread(start = true){
        	var i = 0
            while(i<10){
            	i+=1
                runOnUiThread{
                	textView.text = "카운트 : ${i}"
                }
                Thread.sleep(1000)		// 1000 == 1초
             }
          }
       }
    }

람다식을 이용해서 Thread를 만들었고 runOnUiThread로 TextView에 접근하였다. 사실 runOnUiThread는 간단한 방법이고, 스레드의 결과를 UI에 접근해서 조작하는 구체적인 방법은 Looper(루퍼), Handler(핸들러)를 이용해야 한다. 안드로이드에서는 동기화와 스레드 간섭 문제를 피하기 위해 Handler와 Looper를 사용한다.

Handler(핸들러)

Handler(핸들러)는 각각의 스레드 안에서 만들어 질 수 있고 다른 스레드에서 요청하는 정보를 메세지 큐(Message Queue)를 통해 순서대로 실행시켜 줄 수 있기 때문에 리소스에 대한 동시 접근의 문제를 해결해준다.
핸들러는 메세지 큐를 통해 순차적으로 메인 스레드에서 처리한 메세지를 전달하는 역할을 담당한다.


핸들러는 세가지 단계를 거쳐서 사용된다.

    1. obtainMessage() : 호출의 결과로 메세지 객체를 리턴 받게함.
      (스레드에서 핸들러로 메세지를 보내려면 Message 객체를 사용한다.)
    1. sendMessage() : Message Queue 에 넣는다.
    1. handleMessage() : 메서드에 정의된 기능이 수행됨.
      (sendMessage() 로 메세지를 받고 자동으로 handleMessage가 호출되서 전달된 Message 객체를 처리할 수 있다.)
      이 때 handleMessage 메소드는 메인 스레드에서 실행된다.

핸들러의 기본 생성자는 Looper를 인자로 받거나 비어 있다. 비어있는 핸들러는 자신을 호출한 스레드의 Looper를 사용한다. 만약 메인 스레드에서 핸들러를 인자가 비어있는 생성자로 생성자로 호출하면 이 핸들러는 Main Looper를 사용하고 이 Main Looper를 가진 핸들러가 주로 UI갱신 작업을 할 때 사용된다.

Looper(루퍼)

Looper(루퍼)는 메세지 큐에 저장된 message나 runnable은 Looper가 차례로 꺼내(선입선출:FIFO) 핸들러로 전달한다. 핸들러가 메세지 큐에 넣은 것을 다시 꺼내 핸들러로 전달하는 이유는 message나 runnable을 처리하기 위함이다.

안드로이드는 편리성 제공을 위해 핸들러를 기본 생성자를 통해 루퍼 없이 사용할 수 있게 해준다. 기본 생성자를 통해 핸들러를 생성하면 생성되는 핸들러는 해당 핸들러를 호출한 스레드의 메세지 큐와 Looper에 자동 연결된다.

Main Looper (메인 루퍼)
Looper.getMainLooper()를 이용해서 모든 스레드는 MainLooper에게 Message를 전달할 수 있다. 또는 runOnUiThread를 워크 스레드에서 Main Looper의 메세지 큐에게 메세지를 전달할 수 있다.

음악 재생 코드

아래 코드는 음악을 재생하는 액티비티에서 내부 클래스로 스레드를 생성한 것을 보여준다. 이 스레드로 음악 재생시 재생바가 움직이는 것을 하드코딩하여 만들었다.
playTime : 음악 total 시간, isPlaying : 음악 재생 여부, musicplayerProgressSb : 음악 재생바 songStartTimeTv : 재생 중인 음악이 위치한 시간,

handler를 이용해서 1초마다 워크 스레드에서 메인 루퍼에게 메시지(현재 음악의 재생시간과 음악 재생바의 위치)를 전달한다.

class SongActivity : AppCompatActivity() {

    private lateinit var binding: ActivitySongBinding
    private val song: Song = Song()
    private lateinit var player: Player
    private val handler = Handler(Looper.getMainLooper())
    //스레드를 상속한 내부 클래스
    inner class Player(private val playTime: Int, var isPlaying: Boolean) : Thread() {
        private var second = 0
        override fun run() {
            try {
                while (true) {
                    if (second >= playTime) {
                        break
                    }
                    if (isPlaying) {
                        sleep(1000)
                        second++
                        handler.post {
                            binding.musicplayerProgressSb.progress = second * 1000 / playTime
                            binding.songStartTimeTv.text =
                                String.format("%02d:%02d", second / 60, second % 60)
                        }
                    }
                }
            } catch (e: InterruptedException) {	// 예외처리 (InterruptedException이 일어나면 아래 코드 실행)
                Log.d("inerrupt", "스레드가 종료되었습니다.")
            }
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivitySongBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initSong()

        player = Player(song.playTime, song.isPlaying)
        player.start()

        binding.songDownIb.setOnClickListener { finish() }
        binding.songMiniplayerIv.setOnClickListener {
            player.isPlaying = true
            setPlayStatus(true)
        }
        binding.songPauseIv.setOnClickListener {
            player.isPlaying = false
            setPlayStatus(false)
        }
    }
    

출처 :
developer.android.com/guide/components/process-and-threads?hl=ko
magicalcode.tistory.com/48
recipes4.dev.tistory.com/143
recipes4dev.tistory.com/150
velog.io/@dlrmwl15/안드로이드-스레드Thread와-핸들러Handler
coding-food-court.tistory.com/120

profile
안드로이드 개발 공부

0개의 댓글