Strong 감자의 공부

Ch_ 13 파일 입출력과 스레드 처리 본문

문법_Kotlin

Ch_ 13 파일 입출력과 스레드 처리

ugyeong 2023. 10. 31. 23:00

chapter13 내용 중

02. 스레드에 대해 포스팅 해보겠습니다! 


🌷프로세스와 스레드

- 프로세스 : 특정 프로그램을 task로 보고 이 task를 실행단위로 처리. 특히 멀티스레드를 지원하면서 스레드 단위로 프로그램을 처리할 수 있다.

- 스레드 : 프로세스 내에 작은 단위로 실행할 수 있는 기능. 하나의 프로세스에서 처리되는 스레드는 프로그램을 실행한 스택을 따로 생성하지만, 메모리 등은 프로세스에서 제공하는 것을 공유해서 처리.

- 프로세스의 자원을 사용해서 자기 스레드가 실행되는 동안 다른 스레드를 처리하지 못하게 블로킹한다. 그 다음에 자기 스레드가 블로킹되면 다른 스레드가 실행된다.

- 프로세스는 보통 기본 메인 스레와 다른 작업을 실행하는 별도의 스레드로 관리된다. 별도의 스레드를 생성하지 않으면 메인스레드 하나로 프로그램을 처리한다. 멀티 스레드 처리는 메인 스레드 외에 별도의 스레드를 생성해서 프로그램을 나눠 처리하는 방식이다.

 

🌷프로세스와 스레드 확인 

코어 개수 = Runtime.getRuntime().availableProcessors()

현재스레드 개수 = Thread.currentThread()

 

🌷쓰레드 상태 알아보기 

스레드를 만들려면 Thread 클래스를 상속하거나 Runnable 인터페이스와 Thread 클래스를 사용해서 작성한다.

 

start : 실제 스레드 환경을 구성한 환경을 실행하는 메서드. 이 메서드를 실행해야 내부에 있는 run메서드가 실행된다.

run : 스레드 클래스를 정의할 때 내부에서 실행될 코드를 작성하는 메서드 

join : 스레드가 실행된 다음 종료할 때까지 기다린다. 

sleep : 스레드를 잠시 중단하고 다른 스레드를 처리한 후 다시 자기 스레드를 작동할 수 있게 만든다. 

fun main(){
    println("클래스 참조 :  ${Thread::class}")
    println("인터페이스 참조 : ${Runnable::class}")
    println("현재 스레드 개수 : ${Thread.activeCount()}")
    println(" 현재 스레드 : ${Thread.currentThread()} ")

    fun <T:Any> T.dir():Set<String>{
        val a = this.javaClass.kotlin
        println(a.simpleName)
        val ll = a.members.map { it.name }
        return ll.toSet()
    }

    val tr1 = Thread()
    println("스레드 멤버 개수 : ${tr1.dir().count()}")
    println("스레드 그룹 : ${tr1.threadGroup}")
    println("스레드 생존여부 ${tr1.isAlive}")
    tr1.start()
    println("스레드 생존여부 ${tr1.isAlive}")
    tr1.join()
}
// 결과 
클래스 참조 :  class java.lang.Thread
인터페이스 참조 : class java.lang.Runnable
현재 스레드 개수 : 2
현재 스레드 : Thread[main,5,main]
Thread
스레드 멤버 개수 : 88
스레드 그룹 : java.lang.ThreadGroup[name=main,maxpri=10]
스레드 생존여부 false
스레드 생존여부 true

Process finished with exit code 0

 

🌷스레드 생성 <- 스레드 클래스 상속

스레드를 정의할 때는 run 메서드를 재정의하지만, 내부의 start메서드가 자동 실행되면서 run 메서드를 실행한다. 

-join은 스레드를 처리할 때 메인스레드가 먼저 종료할 수 있으므로 현재 진행 중인 스레드가 모두 처리될 때까지 기다리려고 join메서드를 실행

-실행된 결과를 보면 메인스레드와 보조 스레드를 사용해서 각각의 기능을 처리한 것을 볼 수 있다.

import kotlin.system.exitProcess

fun exec(tr: Thread){
    println("${tr}: 보조 스레드 작동중")
}
class MyThread: Thread(){
    override fun run() {
        super.run()
        val tr = Thread.currentThread()
        exec(tr)
        println("${tr} : 보조 스레드 종료")
    }
}

fun main(){
    val mtr =Thread.currentThread()
    println("${mtr} : 메인스레드 작동중")
    val myThread = MyThread()
    myThread.start()
    exec(myThread)
    println("${mtr}: 대기중")
    myThread.join()
}
// 결과 
Thread[main,5,main] : 메인스레드 작동중
Thread[Thread-0,5,main]: 보조 스레드 작동중
Thread[Thread-0,5,main]: 보조 스레드 작동중
Thread[main,5,main]: 대기중
Thread[Thread-0,5,main] : 보조 스레드 종료

 

🌷스레드 실행하면서 상태 확인 

스레드의 상태여부를 확인하면서 스레드가 작동하는지 알 수 있다.

- 한번 사용한 스레드는 다시 시작할 수 없다, 스레드는 실행이 필요할 때마다 생성해서 start 메서드로 실행해야 한다.

class SimpleThread:Thread(){
    override fun run() {
        super.run()
        println("현재 스레드 이름 : ${this.name}")
    }
}
fun main(){
    println("메인 스레드 : ${Thread.currentThread()}")
    val thread =SimpleThread()
    thread.run()
    println("스레드 활동여부1 : " + thread.isAlive)
    thread.join()
    println("스레드 활동여부2 : " + thread.isAlive)
    thread.run()
    thread.join()
    println("스레드 활동여부3 : " + thread.isAlive) // start를 하지 않았기에 false로만 나온다.

    val th2 = SimpleThread()
    th2.start()
    println("스레드 활동여부4 : " + th2.isAlive)
    th2.join()
    println("스레드 활동여부5 : " + th2.isAlive)
}
// 결과 
메인 스레드 : Thread[main,5,main]
현재 스레드 이름 : Thread-0
스레드 활동여부1 : false
스레드 활동여부2 : false
현재 스레드 이름 : Thread-0
스레드 활동여부3 : false
스레드 활동여부4 : true
현재 스레드 이름 : Thread-1
스레드 활동여부5 : false

 

🌷인터페이스로 스레드 정의 후 스레드 생성 

Runnable 인터페이스를 상속해서 run 메서드를 클래스 내에 구현하고 이 클래스의 객체를 생성해 Thread 클래스의 생성자에 전달해서 스레드 객체를 만든다.

class First:Runnable{
    override fun run() {
        println("Helper 5000 대기중")
        Thread.sleep(5000)
    }
}
class Second:Runnable{
    override fun run() {
        println("Tester 5000 대기중")
        Thread.sleep(5000)
    }
}
fun main(){
    val obj1 = Second()
    val th1 =Thread(obj1)

    val obj2 = First()
    val th2 = Thread(obj2)

    th1.start()
    println("스레드 1 이름 : "+th1.name)
    println("스레드 1 ID : "+ th1.id)
    println("스레드 1 우선순위 : "+ th1.priority)
    println("스레드 1 상태 : "+ th1.state)
    th2.start()
    println("스레드 2 이름 : "+th1.name)
    println("스레드 2 ID : "+ th1.id)
    println("스레드 2 우선순위 : "+ th1.priority)
    println("스레드 2 상태 : "+ th1.state)
    th1.join()
    th2.join()
}

// 결과 
스레드 1 이름 : Thread-0
스레드 1 ID : 16
스레드 1 우선순위 : 5
스레드 1 상태 : BLOCKED

스레드 2 이름 : Thread-0
스레드 2 ID : 16
스레드 2 우선순위 : 5
Tester 5000 대기중
스레드 2 상태 : BLOCKED
Helper 5000 대기중

 

🌷object 표현식으로 스레드 생성 

 인터페이스 상속과 다르게 더 간단히 object 표현식으로 객체를 생성한다. 하지만 스레드 객체를 상속받는 점은 결국 같다.

fun createThread():Thread{
    return object : Thread(){ // 스레드 객체 상속
        override fun run() {
            super.run()
            println("5000 중단 ")
            Thread.sleep(5000)
        }}
}
fun main(){
    val th1 =createThread()
    val th2 = createThread()

    th1.start()
    println("스레드 1 이름 : "+th1.name)
    println("스레드 1 ID : "+ th1.id)
    println("스레드 1 우선순위 : "+ th1.priority)
    println("스레드 1 상태 : "+ th1.state)
    th2.start()
    println("스레드 2 이름 : "+th1.name)
    println("스레드 2 ID : "+ th1.id)
    println("스레드 2 우선순위 : "+ th1.priority)
    println("스레드 2 상태 : "+ th1.state)
    th1.join()
    th2.join()
}

// 결과 
5000 중단
스레드 1 이름 : Thread-0
스레드 1 ID : 16
스레드 1 우선순위 : 5
스레드 1 상태 : TIMED_WAITING
스레드 2 이름 : Thread-0
스레드 2 ID : 16
스레드 2 우선순위 : 5
스레드 2 상태 : TIMED_WAITING
5000 중단

🌷코틀린에서 제공하는 스레드 함수 사용

val myThread = thread(){

println("${Thread.currentThread()} : 보조 스레드 작동" // 스레드 함수에 익명함수 전달

} // 코틀린에서 제공하는 함수로 스레드를 만든다. 이 함수의 인자로 함수를 받으므로 람다표현식으로 함수를 전달한다.

 

🌷사용자 정의 스레드 함수 사용

-스레드 함수의 매개변수는 기본으로 start가 있다. 즉 스레드를 자동으로 실행하는 것인데 isDeamon은 백그라운드에서 스레드를 실행하는 것이다.

 -데몬 :

멀티태스킹 운영체제에서 데몬은 사용자가 직접적으로 제어하지 않고, 백그라운드에서 돌면서 여러 작업을 하는 프로그램이다.

일반 스레드와 비교해서 가장 크게 다른 점은 프로그램이 종료할 때 발생한다. 사용자의 어플리케이션이 종료될 때, 사용자가 생성한 모든 일반 스레드으 수행이 종료되어야 JVM이프로세스가 종료된다. 하지만 데몬은 우선순위가 낮은 스레드로 JVM은 데몬스레드르의 종료를 기다리지 않고 셧다운 작업을 진행한다. 이런 특성으로 종료사 특별한 처리가 필요한 작업의 경우 데몬스레드에서 실행되도 되는지 생각해봐야한다.

1. 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드

2. 주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료된다.

 

import kotlin.concurrent.thread

fun makethread(start: Boolean = true,
               isDamon: Boolean = false,
               contextClassLoader: ClassLoader? = null,
               name: String? = null,
               priority: Int = -1,
               block: () -> Unit
): Thread {
    val thread = object : Thread() {
        override fun run() {
            super.run()
            block()
        }
    }
    if (isDamon) thread.isDaemon = true // 데몬처리
    if (priority > 0) thread.priority = priority
    name?.let { thread.name = it }
    contextClassLoader?.let {
        thread.contextClassLoader = it // 클래스 로더 처리
    }
    return thread
}

fun main() {
    val ss = "스레드 처리 : ${Thread.currentThread()}"
    val th1 = makethread(block = { println(ss) })

    println(th1.javaClass)
    th1.start()
    println("스레드 종료")
    th1.join()
}
// 결과 
class ExcKt$makethread$thread$1
스레드 종료
스레드 처리 : Thread[#1,main,5,main]

 Thread[#1,main,5,main]에서 #1 : 스레드의 이름이 따로 설정되지 않았기 때문에 JVM이 자동으로 할당한 이름입니다. 첫번째 "main"은 메인스레드 이름, "5"는 스레드의 우선순위, 마지막 "main"은 스레드 그룹의 이름 나타냅니다. "main" 스레드 그룹은 JVM이 시작할 때 생성되며, 메인 스레드를 포함하는 기본 스레드 그룹입니다.


 

🌷쓰레드 풀사용

스레드를 무작정 만들면 컴퓨터의 자원을 너무 많이 사용한다. 특정 개수의 스레드를 풀(pool)을 만들어서 특정 스레드를 계속 활용한다.

 

🌷스레드 여러개 만들기

동일한 처리의 스레드를 여러 개 생성해서 리스트 객체에 넣고 이를 내부 순환으로 실행

import kotlin.concurrent.thread

fun main() {
    var count = 0
    val threads = List(10) { // read-only list 
        thread {
            Thread.sleep(10)
            print(".")
            count += 1
            print(count)
        }
    }
    threads.forEach(Thread::join) // 각각의 스레드가 종려될때까지 기다림
}
// 결과
.1.2.3.4.5.6.7.8.9.10

 

🌷스레드 풀을 만들어 스레드 실행 

- 스레드 풀의 스레드는 execute로 실행. 실행할 코드는 람다표현식으로 전달

import java.util.concurrent.Executors

fun main() {
    val executor = Executors.newFixedThreadPool(2) // 특정 스레드 개수만큼만
    var count = 0
    repeat(3) {
        executor.execute {
            Thread.sleep(10)
            println(Thread.currentThread().name)
            count++
            println(count)
        }
    }
    println(executor.isTerminated) // 스레드 풀 미종료
    executor.shutdown() // 스레드 풀 종료
    println(executor.isShutdown) // 스레드 풀 종료 확인
}
// 결과 
false
true
pool-1-thread-2
1
pool-1-thread-1
2
pool-1-thread-2
3

 

🌷스레드 풀을 사용해 처리 

실제 스레드 풀에서 스레드를 실행하는 execute 메서드가  Runnable 객체를 인자로 전달하면 내부적으로 스레드를 실행해준다. 스레드를 5개 만들고 스레드 풀은3개로 지정.

import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

class Task(val name:String) : Runnable{
    override fun run() {
        val d = Date()
        val ft = SimpleDateFormat("hh:mm:ss")
        println("초기 시간 확인"+"task name- "+name+"="+ft.format(d))
        Thread.sleep(100)
    }
}
fun main(){
    val MAX_T=5
    val r1=Task("task1") // 테스크 객체 생성
    val r2=Task("task2")
    val r3=Task("task3")
    val r4=Task("task4")
    val r5=Task("task5")

    val pool = Executors.newFixedThreadPool(MAX_T) // 스레드 풀 생성
    pool.execute(r1) // 풀에서 스레드 실행
    pool.execute(r2)
    pool.execute(r3)
    pool.execute(r4)
    pool.execute(r5)
    pool.awaitTermination(3000L,TimeUnit.MICROSECONDS) // 스레드를 특정 시간까지 대기한 후 종료 
}

// 결과 
초기 시간 확인task name- task4=11:34:15
초기 시간 확인task name- task3=11:34:15
초기 시간 확인task name- task1=11:34:15
초기 시간 확인task name- task2=11:34:15
초기 시간 확인task name- task5=11:34:15

 

🌷스레드 풀을 사용해서 순환 실행 

 스레드 풀을 사용해서 5개의 스레드가 반복해서 사용됨.

import java.util.concurrent.Executors

fun main(){
    val task =object :Runnable{
        override fun run() {
            println("Thread : "+Thread.currentThread().name)
        }
    }
    val service= Executors.newFixedThreadPool(5)
    for(i in 1..10){
        service.submit(task) // 스레드 실행
    }
    service.shutdown()
}
// 결과 
Thread : pool-1-thread-3
Thread : pool-1-thread-2
Thread : pool-1-thread-4
Thread : pool-1-thread-1
Thread : pool-1-thread-5
Thread : pool-1-thread-1
Thread : pool-1-thread-2
Thread : pool-1-thread-4
Thread : pool-1-thread-5
Thread : pool-1-thread-3

 

🌷스레드 하나씩 실행 반환값 처리

스레드가 실행된 결과를 반환받아서 그 결과를 확인하거나 다른 용도로 사용할 수 있다.

- 스레드 풀에서 submit 메서드로 스레드를 실행하면 스레드의 결과를 반환값으로 받을 슈 있다.

- 반환 결과를 get메서드로 가져오면 스레드 반환결과를 확인 할 수 있다. 또한, 이 get 메서드에 특정시간을 넘기면 특정식간을 기다렸다가 정보를 가져온다.

- 반환결과를 cancel 메서드로 스레드를 중단할 수 있다.

import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

fun main() {
    val callable = {
        Thread.sleep(10)
        println("Thread:" + Thread.currentThread().name)
        Thread.currentThread().name+"반환값"
    }
    val executeService = Executors.newFixedThreadPool(2)
    val future1 = executeService.submit(callable)
    println("퓨쳐 객체 :" + future1.javaClass)

    val future2 = executeService.submit(callable)
    val future3 = executeService.submit(callable)

    var value = future1.get() // 작업 끝날때까지 기다려 값 받기
    println("value1 : " +value)
    var canceled = future2.cancel(true) // 작업취소, 취소 여부를 돌려받는다.
    println("canceled : "+canceled)
    value = future3.get(500, TimeUnit.MILLISECONDS) // 500밀리세컨드 동안만 기다려 값 받기
    println("value2 : " +value)
    
}
// 결과
퓨쳐 객체 :class java.util.concurrent.FutureTask
Thread:pool-1-thread-1
Thread:pool-1-thread-2
value1 : pool-1-thread-1반환값
canceled : false
Thread:pool-1-thread-2
value2 : pool-1-thread-2반환값

cancel(true) 현재 실행 중인 경우에도 즉시 중단을 시도해야 함을 나타냅니다. 즉, 스레드에 Interrupt 신호를 보내 작업을 중단시키려고 시도합니다. 그리고 작업이 성공적으로 취소되었으면 true, 그렇지 않으면 false를 반환합니다.
반면에, cancel(false)를 호출하면 현재 실행 중인 작업을 중단하지 않고, 만약 작업이 대기 중이라면 실행되지 않도록 취소합니다. 실행 중인 작업에는 영향을 주지 않습니다.

 

 ❓var canceled = future2.cancel(true) 값이 false인 이유

 

future2.cancel(true) 호출이 취소에 실패한 이유는 future2가 이미 실행되었거나 실행 중이었기 때문일 가능성이 높습니다.
위 코드에서 future1, future2, future3 모두 거의 동시에 제출(submit)됩니다. 고정 스레드 풀의 크기가 2이기 때문에 future1과 future2는 거의 동시에 실행을 시작할 것입니다. 따라서 future2.cancel(true)가 호출될 때, future2는 이미 실행 중일 가능성이 높습니다.
cancel(true) 메서드는 실행 중인 작업을 중단하려고 시도하지만, 작업 내부에서 인터럽트 상태를 적절하게 처리하지 않으면 실제로 중단되지 않을 수 있습니다.

-chat gpt 센빠이가 알려주셨다.

 

 

참고 ) 데몬 - https://hbase.tistory.com/285