Thread..?
안드로이드 앱 개발 뿐만이 아니라 컴퓨터 언어로 무언가를 개발하는 모든 개발자들은 Thread에 대해 듣게 된다.
이 Thread 란 무엇일까?
Process 와 Thread
Process 는 간단하게 하나의 프로그램을 의미한다.
그리고 Thread는 이 Process 내에서 작업을 수행하는 주체이다.
아무것도 동작하지 않는 Process(프로그램) 은 프로그램이라고 할 수 없다. 아주 간단한 기능이라도 어떤 기능 한가지는
무조건 수행하는데, 이러한 작업은 Thread에서 수행한다.
따라서, Process에는 무조건 한개 이상의 Thread가 존재한다.
안드로이드 앱 개발에는 UI Thread(Main Thread)가 무조건 존재하고, 개발에 따라 다수의 Worker Thread 가 존재하게 된다.
메모리 구조적 측면에서 본 Thread
어떤 프로그램이 실행되기 위해서는 메모리에 로드되어야 한다. 메모리에 로드되지 않은 프로그램은 그냥 단순한 코드 덩어리일 뿐이다.
메모리에 로드 된다는 것은 작성한 코드에 필요한 메모리공간이 할당된다는 의미인데, 이 메모리 공간은
Code, Data, Heap, Stack 영역으로 이루어져 있다.
여기서 Thread는 각각의 고유한 Stack 영역을 가지고 전체 프로세스에서 Code, Data, Heap 영역을 공유한다.
이를 공유자원 이라 하며, 이 공유자원을 관리하기 위해 뮤텍스나 세마포어 등의 개념이 나온다(이 글에선 다루지 않는다)
동기? 비동기?
Thread 에 대해 이야기하면 반드시 동기 와 비동기에 대해 듣게 될 것이다.
동기(Syncronous) 는 작업의 흐름이 순서대로 이루어지는 것을 말한다. 가령 A라는 작업 뒤에 B라는 작업을 하라고 했다면, A라는 작업이 끝나야 B라는 작업이 실행된다. A 작업이 오래걸리면 오래걸릴수록 B 라는 작업을 시작하지 못하고 A가 끝날때까지 기다려야 하는 것이다.
안드로이드 개발하면서 UI Thread(Main Thread)에 시간이 오래 걸리는 작업을 하지 말라고 하는 이유도 여기에 있다.
UI Thread 는 화면을 그리고 사용자와 상호작용 해야한다(일정시간 내에 화면을 그리지 못하거나 상호작용 하지 못한다면 ANR 에러를 초래한다..!). 만일, UI Thread에서 위에서 말한 A작업과 B작업을 동기식으로 수행한다고 하면, A작업이 오래걸리는동안 UI Thread 는 B 작업을 수행하지 않고 대기하게 될 것이고, 화면은 멈춰있을 것이다.
사용자는 앱이 작동하지 않는다고 생각할 것이고, ANR 오류로 인해 앱이 죽는것을 보게 될 것이다
(그러고 플레이스토어에 1점이 달리겠지...)
비동기(Asyncronous) 는 코드 작업이 동시에 진행되는 것을 말한다. 아래 사진을 보면 이해가 빠를 것이다.
동기 방식의 경우 카운터를 보는 직원이 한명이라 손님이 주문을 하기 위해 줄을 서서 기다린다. 앞사람 주문이 오래걸리면 그만큼 뒷 사람은 기다려야 한다. 반면 비동기 방식의 경우 카운터를 보는 직원이 여러명이기 때문에 동시에 주문이 가능하다.
Thread의 상태
Thread는 생성부터 종료까지 다양한 상태를 가지게 된다. 이는 JVM에 의해 생성되고 관리된되며, Thread클래스의 State Enum으로 존재한다.
- NEW : Thread가 생성되었지만, 시작되지 않은 상태이다.
- RUNNABLE : JVM에 의해 실행가능한 상태의 Thread이다. 그러나 processor와 같은 os로부터 다른 리소스를 기다리고 있는 상태일 수도 있다.
- BLOCKED : 모니터락(고유락)을 기다리는 차단된 상태의 Thread이다. 공유객체의 synchronized 메소드나 synchronized 블록을 수행하는 Thread가 있는 상황에서 이 공유객체에 접근하는 다른 Thread들이 얻게되는 상태 변화이다.(이후 자세히 알아볼 것이다)
- WAITING : 다른 Thread의 작업 완료를 기다리는 상태이다. wait() 나 join()을 사용하면 이뤄지는 상태 변화이다.
- TIMED_WAITING : 일정시간 기다리는 상태이다. 주로 sleep(long mills) 를 이용했을 때 이뤄진다.
- TERMINATED : Thread의 실행이 완료된 상태이다.
Thread 의 함수
- start() : Thread 를 시작한다.
- run() : Thread에서 실행할 작업을 정의한다. Runnable Interface에 추상함수로 정의되어있으며, Thread클래스를 만들면 오버라이딩하여 사용한다.
- sleep(long mills) : 현재 Thread를 mills 시간만큼 중지한다.
- yield() : 현재 스레드를 일시중지하고, 다른 스레드에게 실행기회를 양보한다.
- join() : join() 을 호출한 Thread가 종료될때까지 join이 호출된 Thread를 중지한다.
(a.join() 이 Main Thread에서 호출되었다면 a Thread 가 종료될때까지 Main Thread 를 중지한다) - interrupt() : Thread 작업을 중단한다.
- isAlive() : Thread가 실행중인지 여부를 반환한다(Boolean)
- setName(String name) / getName() : Thread의 이름을 지정한다 / Thread의 이름을 반환한다
- setPriority(int priority) / getPriority() : Thread의 우선순위를 지정한다 / Thread의 우선순위를 반환한다
Thread 사용해보기
Thread 를 상속하여 생성하는 방법과 Runnable 인터페이스를 구현하는 방식 2가지로 사용이 가능하다.
자바나 코틀린은 다중상속을 지원하지 않기때문에 Thread를 상속하여 생성하는 방법은 다른 클래스를 상속할 수 없지만, Runnable 인터페이스를 구현하는 방법은 다른 클래스의 상속이 가능하고, 재사용이 가능하다는 장점이 있다.
일반적으론 Runnable 인터페이스를 구현하는 방법을 사용하는데, 이는 재사용성이 높고, 코드의 일관성을 유지할 수 있기 에 보다 객체지향적인 방법이기 때문이다.
먼저, Thread를 상속하여 생성하는 방법이다. 버튼을 누르면 Thread가 동작하게 하였다.
inner class ExThread() : Thread(){
override fun run() {
for(i in 0..10){
Log.d("shhan", "출력 : $i")
}
Log.w("shhan", "스레드 종료")
}
}
0부터 10까지 차례대로 출력하고, "스레드 종료" 라는 로그를 출력한다.
다음과 같이 생성할 수도 있다. 코틀린의 객체 표현식을 사용한 방법이다.
val exThread2 = object : Thread(){
override fun run() {
for(i in 0..10){
Log.d("shhan", "출력 : $i")
}
Log.w("shhan", "스레드 종료")
}
}
버튼 클릭 리스너는 간단하게 다음과 같이 하였다.
root.findViewById<AppCompatButton>(R.id.btn_start_thread2).apply{
setOnClickListener{
// 객체 표현식 사용
/*val exThread2 = object : Thread(){
override fun run() {
for(i in 0..10){
Log.d("shhan", "출력 : $i")
}
Log.w("shhan", "스레드 종료")
}
}*/
val exThread = ExThread()
Log.d("shhan", "스레드 시작")
exThread.start()
// 객체 표현식 사용
// exThread2.start()
Log.d("shhan", "클릭리스너 종료")
}
}
Main Thread의 실행과 exThread의 실행이 동시에 이루어지므로, "스레드 시작" 로그 찍히고 Main Thread에서
"exThread 시작해~" 라고 한뒤, "클릭리스너 종료" 로그를 띄운다.
exThread는 시작하라는 말을 듣고 동작하기 시작하여 0부터 10까지 출력하고 "스레드 종료" 라는 로그를 띄운다.
비동기 방식이기 때문에 스레드가 끝난뒤에 "클릭리스너 종료" 로그가 찍히지 않는다.
만일, exThread가 끝난뒤에 "클릭리스너 종료" 라는 로그를 띄우고 싶다면, 위에서 살펴본 Thread의 함수 중 join() 을 사용하면 된다.
root.findViewById<AppCompatButton>(R.id.btn_start_thread2).apply{
setOnClickListener{
// 객체 표현식 사용
/*val exThread2 = object : Thread(){
override fun run() {
for(i in 0..10){
Log.d("shhan", "출력 : $i")
}
Log.w("shhan", "스레드 종료")
}
}*/
val exThread = ExThread()
Log.d("shhan", "스레드 시작")
exThread.start()
// 객체 표현식 사용
// exThread2.start()
exThread.join()
Log.d("shhan", "클릭리스너 종료")
}
}
join을 호출한 Main Thread는 exThread가 종료될때까지 중단됬다가, exThread 종료 후 동작한다.
Runnable 인터페이스를 구현하는 방법은 다음과 같다. 기능은 동일하니, 어떻게 생성하는지를 눈여겨 보면 될것이다
inner class Runnable1() : Runnable{
override fun run() {
for(i in 0..10){
Log.d("shhan", "출력 : $i")
}
Log.w("shhan", "스레드 종료")
}
}
// 이후 Thread를 실행하기 전 Runnable1 클래스를 Thread클래스의 인자로 넣어 Thread를 생성해준다.
val thread1: Thread = Thread(Runnable1())
thread1.start()
Runnable 인터페이스를 구현하여 Runnable1 클래스를 생성하고, 이를 Thread()의 인자에 넣어 Thread 객체를 생성한다.
이또한 객체표현식으로 가능하다.
// Thread의 인자로 들어갈 Runnable을 객체 표현식으로 바꾼 모습
val thread1: Thread = Thread(object : Runnable{
override fun run() {
for(i in 0..10){
Log.d("shhan", "출력 : $i")
}
}
})
결과는 동일하다.
다음 코드는 Thread의 특징을 알 수 있는 코드이다.
Main Thread에서 4개의 Thread 를 for문을 통해서 생성한 뒤 차례대로 실행해준다.
그리고 종료되는 시점을 확인해보자.
Thread에서 하는 동작은
inner class ExThread(seq: Int) : Thread(){
private val seq: Int
init{
this.seq = seq
}
override fun run() {
Log.i("shhan", "$seq 번째 스레드 시작")
try{
sleep(1000)
} catch(e: Exception){
e.printStackTrace()
}
Log.w("shhan", "$seq 번째 스레드 종료")
}
}
인자로 Int형 숫자를 받아 몇번째 Thread가 시작하는지 로그로 찍은 다음에 1초 중단 후 몇번째 스레드가 종료됬는지 로그로 찍는다.
Main에서는
root.findViewById<AppCompatButton>(R.id.btn_start_thread2).apply{
setOnClickListener{
for(i in 1..10){
val exThread = ExThread(i)
exThread.start()
}
Log.d("shhan", "Main Thread End")
}
}
1부터 10까지 Thread를 생성한 뒤 차례대로 실행해주고, for문이 끝나면 "Main Thread End" 라는 로그를 출력한다.
결과를 확인해보면
분명 for문으로 1부터 10까지 Thread생성 -> 시작, 생성->시작 ...을 반복했지만, 정작 로그에 찍힌 결과는 스레드가 랜덤으로 실행된 것을 확인할 수 있다.
이를 통해 다음 2가지를 알 수 있다.
- Thread는 반복문의 순서대로 생성되었지만, 생성된 순서대로 실행되는 것은 아니다.
- Thread 의 종료는 시작한 순서대로 이루어 지지 않는다.
이상으로 Thread의 기본적인 내용에 대해 알아보았다.
추후 더 공부해서 뮤텍스, 세마포어, 모니터락 등등에 대해 포스팅 할것이다.
참고 링크 & 이미지 출처
https://www.tcpschool.com/java/java_thread_concept
https://adjh54.tistory.com/167
https://defacto-standard.tistory.com/1191
https://handr95.tistory.com/37