ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kotlin 문법 강의] 5주차: Kotlin 심화 (스레드(Thread), 코루틴(Coroutine))
    안드로이드 2024. 3. 13. 14:49

    ✏️ TIL(Today I Learned)

    스레도와 코루틴에 대해 배우면서, TextRpgGame에 스레드를 사용하는 기능을 추가했다. 

     

    main()에서 경마를 선택한 뒤, 아래 코드로 말의 번호를 문자열로 입력받으면

     "selectHorse" -> {
        println("말의 번호를 입력해주세요(1번 혹은 2번)")
        while(true) {
            try {
                var originName = readLine()
                if(originName?.equals("1번") == true || originName?.equals("2번") == true) {
                    return originName
                } else {
                    println("말의 이름을 다시 입력해주세요")
                }
            } catch(e:Exception) {
                println("말의 이름을 다시 입력해주세요")
            }
        }
    }
    
    fun startLotto(character: Character, horse: String) {
        var cashShop = CashShop.getInstance()
        cashShop.startLotto(character, horse)
    }

     

    그 값을 매개변수로 startLotto()를 호출한다.

    CashShop 클래스에서 두개의 스레드를 새로 만들어서 각 말의 현재 위치를 3초마다 출력하게 했다.

    위치는 랜덤하게 더해져서, 먼저 finalDst가 100 넘는 말의 번호가 입력받은 것과 같으면
    캐릭터의 money에 10000을 더한다.

    fun startLotto(character: Character, selectHorse: String) {
            var random = Random()
            val finalDst = 100
            isFinish = false
            thread(start = true) {
                var currentPosition = 0
                while(currentPosition < finalDst && isFinish == false) {
                    currentPosition += (random.nextInt(5) + 1)
    
                    println("1번말 현재 위치: ${currentPosition}m")
                    runBlocking {
                        launch {
                            delay(300)
                        }
                    }
                }
                if(lottoStatus == null || lottoStatus != "2번") {
                    lottoStatus = "1번"
                    isFinish = true
                    println("1등: ${lottoStatus}말")
    
                    if(lottoStatus.equals(selectHorse)) {
                        println("축하합니다! 당첨!")
                        println("상금으로 1만원 지급")
    
                        if(character is Archer) {
                            character?.run {
                                money += 10000
                            }
                        } else if(character is Wizard) {
                            character?.run {
                                money += 10000
                            }
                        }
                    }
                }
            }
    
            thread(start = true) {
                var currentPosition = 0
                while(currentPosition < finalDst && isFinish == false) {
                    currentPosition += (random.nextInt(10) + 1)
    
                    println("2번말 현재 위치: ${currentPosition}m")
                    // 위와 동일하므로 생략
                }
            }
        }

     

    📝 공부한 Kotlin 요약 정리

    비동기 프로그래밍)

    비동기(Asynchronous) 프로그래밍: 작업을 동시에 수행하거나, 한 작업이 완료될 때까지 기다리지 않고 다른 작업을 수행하는 방식이다. 이는 특히 I/O 작업, 네트워크 통신, 데이터베이스 액세스 등과 같이 시간이 오래 걸리는 작업에 유용하다. 비동기 프로그래밍은 프로그램의 성능을 향상시키고, 응답성을 높이며, 블로킹을 최소화하는 데 도움이 된다.

    • 동기와 비동기 비교
      • 동기 프로그래밍
        - 요청 보내고 결과값을 받을 때까지 작업을 멈춤
        - 한 가지씩 작업을 처리
      • 비동기 프로그래밍
        - 요청 보내고 결과값을 받을 때까지 멈추지 않고,  다른 일을 수행
        - 다양한 일을 한 번에 수행

    스레드(Thread)

    스레드(Thread): 컴퓨터 프로세스 내에서 실행되는 독립적인 실행 흐름이다. 프로세스 안에서 더 작은 작업의 단위를 스레드라고한다. 각 스레드는 프로세스 내의 공유 자원에 대한 독립적인 스택과 레지스터를 가지고 있다. 스레드는 작업을 수행할때 각 독립된 메모리 영역인 스택을 가지므로, 스택메모리의 일정 영역을 차지한다. 스레드는 운영체제에 의해 관리되며, 다수의 스레드가 동시에 실행될 수 있다.

    • 스레드의 주요 특징:
      • 독립성(Independence):
        각 스레드는 독립적인 실행 흐름을 가지며, 하나의 스레드가 다른 스레드에게 영향 X
      • 공유 자원(Shared Resources):
        하나의 프로세스 내에서 실행되는 스레드들은 메모리 공간이나 파일 등의 자원을 공유 O 이로 인해 효율적인 자원 활용이 가능
      • 경량성(Lightweight):
        스레드는 프로세스 내부에 생성되기 때문에 프로세스와 비교해 상대적으로 가벼움
        스레드 간 전환은 프로세스 간 전환보다 빠름
      • 프로세스와 스레드의 관계:
        하나의 프로세스는 여러 개의 스레드로 구성 가능
        프로세스 내의 각 스레드는 같은 코드와 자원을 공유하면서 각각 독립적으로 실행 가능
      • 동시성(Concurrency):
        다수의 스레드가 동시에 실행될 수 있어 여러 작업을 동시에 수행 O
        이로 인해 병렬성을 활용하여 성능을 향상 가능
      • 스레드의 상태(State):
        • 실행(Running): 현재 CPU에서 명령어를 실행 중인 상태.
        • 대기(Waiting): 어떤 이벤트를 기다리는 상태.
        • 준비(Ready): CPU에서 실행 가능한 상태이지만 아직 실행되지 않은 상태.
        • 종료(Terminated): 스레드의 실행이 완료된 상태.
      • @Volatile: 키워드 중 하나로, 변수의 값을 메모리에서 읽거나 쓸 때 특별한 주의가 필요함을 표시
        이 키워드를 사용하면 해당 변수의 값이 항상 메인 메모리에서 읽히고 쓰이도록 보장한다.
        스레드 간의 가시성(visibility) 문제를 해결하기 위한 목적으로 사용한다.

    • 스레드 사용 예시:
      // build.gradle(:app)에 외부 종속성 추가해야 사용가능
      
      dependencies {
          implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
      }​​
      fun main() {
          thread(start = true) {
              for(i in 1..10) {
                  println("Thread1: 현재 숫자는 ${i}")
                  runBlocking {
                      launch {
                          delay(1000)
                      }
                  }
              }
          }
      
          thread(start = true) {
              for(i in 50..60) {
                  println("Thread2: 현재 숫자는 ${i}")
                  runBlocking {
                      launch {
                          delay(1000)
                      }
                  }
              }
          }
      }​

     

    코루틴(Coroutines)

    코루틴 (Coroutines): 비동기 작업을 위한 경량 스레드(Light-weight thread)이다.

    코드의 일부를 일시 중단하고 나중에 재개할 수 있는 구조이다. suspend 키워드를 사용하여 일시 중단 가능한 함수를 선언하고, launch, async 등의 빌더를 사용하여 코루틴을 실행한다.

    • 코루틴 특징:
      • 최적화된 비동기 함수를 사용
      • 하드웨어 자원의 효율적인 할당을 가능
      • 안정적인 동시성, 비동기 프로그래밍을 가능
      • 쓰레드보다 더욱 가볍게 사용 가능
      • 로직들을 협동해서 실행하자는게 목표
    • 주요 코루틴 빌더
      • launch 빌더:
        가장 일반적으로 사용되는 코루틴 빌더 중 하나
        반환값이 없으며, 그 자체로 독립적으로 실행
        Job객체로 코루틴을 관리
        Job객체 함수 (join: 현재의 코루틴이 종료되기를 기다림 / cancel: 현재의 코루틴을 즉시 종료)
      • async 빌더:
        async 빌더는 Deferred를 반환하며, 이를 통해 비동기 작업의 결과값을 얻는다.
        await 함수를 사용하여 결과값을 기다릴 수 있다.
      • runBlocking 빌더:
        runBlocking 빌더는 메인 함수나 테스트 코드에서 코루틴을 사용할 때 주로 사용
        메인 함수를 코루틴으로 변환하여 비동기 코드를 사용할 수 있다.
      • withContext 빌더:
        withContext 빌더는 특정한 코루틴 컨텍스트에서 코드 블록을 실행
        주로 스레드 풀이나 다른 디스패처에서 코드를 실행할 때 사용
    • 스코프로 범위 지정
      • GlobalScope:
        애플리케이션이 실행되는 동안 지속적으로 계속 동작해야 하는 코루틴을 생성할 때 사용
        전역 스코프에서 실행되는 코루틴은 애플리케이션 수명 주기 동안 지속되며, 앱이 종료될 때까지 실행 가능
        주로 애플리케이션의 전역적인 비동기 작업이나 백그라운드 작업을 처리하는 데 활용
      • CoroutineScope:
        특정한 스코프 내에서 필요한 코루틴을 생성하고 관리할 때 사용
        일반적으로 액티비티, 프래그먼트, 또는 특정한 기능을 수행하는 블록 내에서 만들어 사용
        스코프가 끝나면 해당 스코프 내에서 실행되던 모든 코루틴이 자동으로 취소됨
        import kotlinx.coroutines.*
        
        fun main() {
            // Example using GlobalScope
            exampleUsingGlobalScope()
        
            // Example using CoroutineScope
            runBlocking {
                exampleUsingCoroutineScope()
            }
        }
        
        fun exampleUsingGlobalScope() {
            // GlobalScope를 사용하여 전역 스코프에서 코루틴 생성
            GlobalScope.launch {
                delay(1000) // 비동기 작업 수행
                println("GlobalScope: 코루틴이 실행됨")
            }
        
            // 메인 스레드는 바로 종료되지 않고, 코루틴이 실행될 때까지 기다림
            Thread.sleep(2000)
        }
        
        suspend fun exampleUsingCoroutineScope() {
            // CoroutineScope를 사용하여 특정 스코프에서 코루틴 생성
            coroutineScope {
                launch {
                    delay(1000) // 비동기 작업 수행
                    println("CoroutineScope: 코루틴이 실행됨")
                }
                // 다른 작업들...
            }
            // CoroutineScope에서 생성된 코루틴이 완료될 때까지 기다림
        }

    • 코루틴을 실행할 스레드 지정
      Dispatcher: 코틀린 코루틴에서 코루틴이 실행되는 스레드나 스레드 풀을 지정한다.
      코루틴은 특정 스레드에서 실행되지 않고, 다양한 디스패처를 사용하여 적절한 스레드 풀에서 실행된다. 코루틴 디스패처는 주로 launch, async, withContext 등의 코루틴 빌더에서 사용되며, 다양한 종류가 있습니다.

      주요 디스패처:
      • Dispatchers.Main:
        UI와 상호작용하기 위한 메인스레드
      • Dispatchers.IO:
        네트워크나 디스크 I/O작업에 최적화되어있는 스레드
      • Dispatchers.Default:
        기본적으로 CPU최적화되어있는 스레드
        코루틴이 자동으로 스레드 풀에서 실행됨
        println("메인스레드 시작")
        // CoroutineScope를 사용하여 전역 스코프에서 코루틴을 생성
        var job = CoroutineScope(Dispatchers.Default).launch {
            // async를 사용하여 파일 다운로드 코루틴 생성
            var fileDownloadCoroutine = async(Dispatchers.IO) {
                delay(10000)
                "파일 다운로드 완료"
            }
        
            // async를 사용하여 데이터베이스 연결 코루틴 생성
            var databaseConnectCoroutine = async(Dispatchers.IO) {
                delay(5000)
                "데이터베이스 연결 완료"
            }
        
            // 각 코루틴의 결과를 출력
            println("${fileDownloadCoroutine.await()}") // 파일 다운로드 완료
            println("${databaseConnectCoroutine.await()}") // 데이터베이스 연결 완료
        }
        
        // 모든 작업이 완료될 때까지 메인 스레드를 블록
        runBlocking {
            job.join()
        }
        
        println("메인스레드 종료")
        // 코루틴을 취소
        job.cancel()

     

    레드와 코루틴)

    [스레드]

    • 작업 하나하나의 단위 : Thread
      • 각 Thread 가 독립적인 Stack 메모리 영역을 가짐
    • 동시성 보장 수단 : Context Switching
      • 운영체제 커널에 의한 Context Switching 을 통해 동시성을 보장해요
      • 블로킹 (Blocking):
        - Thread A가 Thread B 의 결과를 기다리고 있어요
        - 이 때, Thread A는 블로킹 상태라고 할 수 있어요
        - A는 Thread B 의 결과가 나올 때 까지 해당 자원을 사용하지 못해요
    • 예시 
      • Thread A가 Task 1을 수행하는 동안 Task 2 의 결과가 필요하면 Thread B를 호출
      • 이때 Thread A는 블로킹 되고 Thread B로 프로세스간에 스위칭이 일어나 Task 2을 수행
      • Task 2가 완료되면 Thead A로 다시 스위칭해서 결과 값을 Task 1에게 반환
      • 이때 Task 3, Task 4는 A, B작업이 진행되는 도중에 멈추지 않고 각각 동시에 실행됨
      • 이때 컴퓨터 운영체제 입장에서는 각 Task를 쪼개서 얼마나 수행할지가 중요
      • 그래서 어떤 쓰레드를 먼저 실행해야할지 결정하는행위를 스케쥴링이라고 함
      • 이러한 행위를 통해 동시성을 보장

    [코루틴]

    • 작업 하나하나의 단위 : Coroutine Object
      • 여러 작업 각각에 Object를 할당
      • Coroutine Object 도 엄연한 객체이기 때문에 JVM Heap에 적재
    • 동시성 보장 수단 : Programmer Switching (No-Context Switching)
      • 소스 코드를 통해 Switching 시점을 마음대로 정해요 (OS는 관여 X)
      • Suspend (Non-Blocking)
        - Object 1이 Object 2의 결과를 기다릴 때 Object 1의 상태는 Suspend로 바뀜
        - 그래도 Object 1을 수행하던 Thread는 그대로 유효
        - 그래서 Object 2도 Object 1과 동일한 Thread에서 실행
    • 예시
      • Coroutine은 작업 단위가 Object
      • Task 1을 수행하다가 Task 2의 수행요청이 발생했다고 가정함
      • 신기하게도 컨텍스트 스위칭 없이 동일한 Thread A에서 수행 O
      • Thread C처럼 하나의 쓰레드에서 여러 Task Object들을 동시에 수행 O
      • 이러한 특징때문에 코루틴 = Light-Weight Thread

    [정리]

    • 둘 다 각자의 방법으로 동시성을 보장하는 기술
    • 코루틴은 Thread를 대체하는 기술 X
    • 하나의 Thread를 더욱 잘개 쪼개서 사용하는 기술 O
    • 코루틴은 쓰레드보다 CPU 자원을 절약하기 때문에 Light-Weight Thread라고 한다.
    • 구글에서는 코틀린의 코루틴 사용을 적극 권장한다.

     

    🔎 전체 코드

     

    WorldMain.kt

    fun main() {
        val worldName = "스코월드"
    	//생략
        // 모든 조건을 통과한 경우에만 환영
        if(isNamePass && isAgePass && isJobPass) {
            // 새로 이름 추가
            names.add(myName)
            displayInfo(worldName, myName, myAge, myJob)
    
            if(myJob == "마법사") {
            // 생략
            } else if(myJob == "궁수") {
                println("궁수를 선택했군요")
                var myCharacter = Archer(myName, myAge, myGender, myMoney, myHp)
    
                while(true) {
                    println("[1] 슬라임동굴, [2] 좀비마을, [3] 캐쉬샵, [4] 로또 [5] 종료")
                    var selectNumber= inputMyInfo("selectNumber").toString().toInt()
    
                    when(selectNumber) {
                        1 -> {
                            selectWorld(1, myCharacter)
                        }
                        2 -> {
                            selectWorld(2, myCharacter)
                        }
                        3 -> {
                            openCashShop(myCharacter)
                        }
                        ///////////////////새로 추가/////////////////////
                        4 -> {
                            var selectHorse = inputMyInfo("selectHorse").toString()
                            startLotto(myCharacter, selectHorse)
                        }
                        5 -> {
                            println("게임 종료")
                            break
                        }
                        else -> {
                            break
                        }
                    }
                }
            }
        }
    }
    
    //생략
    
    fun inputMyInfo(type:String): Any? {
        return when(type) {
            "name" -> {
            //생략
            ///////////////////새로 추가/////////////////////
            "selectHorse" -> {
                println("말의 이름을 입력해주세요")
                while(true) {
                    try {
                        var originName = readLine()
                        if(originName?.equals("one") == true || originName?.equals("two") == true) {
                            return originName
                        } else {
                            println("말의 이름을 다시 입력해주세요")
                        }
                    } catch(e:Exception) {
                        println("말의 이름을 다시 입력해주세요")
                    }
                }
            }
            else -> {
                return "no"
            }
        }
    }
    
    fun startLotto(character: Character, horse: String) {
        var cashShop = CashShop.getInstance()
    
        cashShop.startLotto(character, horse)
    }

     

    CashShop

    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    import java.util.Random
    import kotlin.concurrent.thread
    
    class CashShop private constructor() {
        private val bowPrice = 150
        private val staffPrice = 120
    
        companion object {
            @Volatile private var instance: CashShop? = null
            @Volatile private var lottoStatus: String? = null
            @Volatile private var isFinish: Boolean? = null
    
            fun getInstance(): CashShop {
                // 외부에서 요청왔을때 instance가 null인지 검증
                if(instance == null) {
                    // synchronized로 외부 쓰레드의 접근을 막음
                    synchronized(this) {
                        instance = CashShop()
                    }
                }
                return instance!!
            }
        }
    
        fun startLotto(character: Character, selectHorse: String) {
            var random = Random()
            val finalDst = 100
            isFinish = false
            thread(start = true) {
                var currentPosition = 0
                while(currentPosition < finalDst && isFinish == false) {
                    currentPosition += (random.nextInt(5) + 1)
    
                    println("1번말 현재 위치: ${currentPosition}m")
                    runBlocking {
                        launch {
                            delay(1000)
                        }
                    }
                }
                if(lottoStatus == null || lottoStatus != "two") {
                    lottoStatus = "one"
                    isFinish = true
                    println("1등: ${lottoStatus}말")
    
                    if(lottoStatus.equals(selectHorse)) {
                        println("축하합니다! 당첨!")
                        println("상금으로 1만원 지급")
    
                        if(character is Archer) {
                            character?.run {
                                money += 10000
                            }
                        } else if(character is Wizard) {
                            character?.run {
                                money += 10000
                            }
                        }
                    }
                }
            }
    
            thread(start = true) {
                var currentPosition = 0
                while(currentPosition < finalDst && isFinish == false) {
                    currentPosition += (random.nextInt(10) + 1)
    
                    println("2번말 현재 위치: ${currentPosition}m")
                    runBlocking {
                        launch {
                            delay(1000)
                        }
                    }
                }
                if(lottoStatus == null || lottoStatus != "one") {
                    lottoStatus = "two"
                    isFinish = true
                    println("1등: ${lottoStatus}말")
                    if(lottoStatus.equals(selectHorse)) {
                        println("축하합니다! 당첨!")
                        println("상금으로 1만원 지급")
    
                        if(character is Archer) {
                            character?.run {
                                money += 10000
                            }
                        } else if(character is Wizard) {
                            character?.run {
                                money += 10000
                            }
                        }
                    }
                }
            }
        }
    }
Designed by Tistory.