Kotlin 문법 및 연습 문제

Kotlin - 비동기 프로그래밍 (쓰레드, 코루틴)

돗개진 2024. 3. 12. 20:30

비동기 프로그래밍이란?

: 여러가지 로직들이 완료 여부와 관계 없이 실행되는 방식 (쉽게 설명하면 정해진 순서 없이 동시에 수행)

 

동기 프로그래밍

: 요청을 보낸 로직이 완료되어 결과값을 받을 때까지 다음 작업은 멈춘 상태로 기다림

 

쓰레드(Thread)

: 로직을 동시에 실행할 수 있도록 돕는 것이 목적이다. 프로그램은 하나의 main 쓰레드가 존재하며 별도의 자식 쓰레드를 thread 키워드로 생성 가능하다.

: 동시성 보장 수단: Context  Switching, 운영체제 커널에 의해 동시성 보장 / 작업 단위 = Thread / 각 Thread가 독립적인 Stack 메모리를 가짐

: 블로킹(Blocking) : Thread A 가 Thread B 의 결과를 기다리고 있으면 A는 블로킹 상태며 B의 결과가 나올 때까지 해당 자원을 사용할 수 없다.

: 스케쥴링(Scheduling) : 어떤 Thread를 먼저 실행할지 결정하는 행위

 

프로세스(Proceess)

: 메모리에 프로그램이 올라가서 실행될 때 이를 프로세스 1개라고 볼 수 있다. 쓰레드는 프로세스 안에서 더 작은 작업 단위이며 쓰레드가 생성되어 수행될 때 각 독립적인 메모리 영역인 stack을 가진다.

 

Process 안 더 작은 단위인 Thread1,2,3과 이들이 stack의 형태로 메모리 영역을 가지는 모습을 이미지화한 자료

 

fun main() {
    thread(start = true) {
        for(i in 1..10) {
            println("Thread1: 현재 숫자는 ${i}")
            runBlocking {
                launch {
                    delay(1000) // 1초 단위로 딜레이
                }
            }
        }
    }
 // !! 누가 먼저 cpu 자원 할당할지 정해져 있지 않음 !!
    thread(start = true) {
        for(i in 50..60) {
            println("Thread2: 현재 숫자는 ${i}")
            runBlocking {
                launch {
                    delay(1000)
                }
            }
        }
    }
}

ㄴ> 위 코드를 실행하면 Thread1 과 Thread2 는 실행할 때마다 서로 순서를 갖지 않고 출력되는 것을 확인할 수 있다. 따라서 누가 먼저 cpu 자원을 할당할지 정해져 있지 않는 것이다.

 


 

코루틴(Coroutine)

: 쓰레드보다 더 가벼운 비동기 프로그래밍을 지원한다. 최적화 된 비동기 함수를 사용 (로직 협동 실행 + 빌더)

: 동시성 보장 수단: Programmer Switching (No-Context Switching) / 소스 코드를 통해 Swithcing 시점을 마음대로 정하며 OS가 관여하지 않음 / Suspend(Non-Blocking)

: launch & async 빌더를 가장 많이 사용한다

 

launch

: 결과값이 없는 코루틴 빌더 (Job 객체로 코루틴을 관리함) 

 

async

: 결과값이 있는 코루틴이며 Defferd 타입으로 값을 리턴함

 

코루틴은 'Scope'로 범위 지정이 가능

- GlobalScope : 앱이 실행된 이후에 계속 수행되어야 할 때 사용

- CoroutineScope: 필요할 때만 생성하고 사용 후에는 정리 필요

 

코루틴을 실행할 쓰레드를 'Dispatcher'로 지정 가능

-  Dispatchers.Main : UI 상호작용 메인 쓰레드 / Dispatchers.IO : 네트워크나 디스크 I/O 작업(외부) / Dispatchers.Default : 기본적인 CPU 최적화 쓰레드

 

fun main(args: Array<String>) {
    println("메인쓰레드 시작")
    var job = GlobalScope.launch {
        delay(3000)
        println("여기는 코루틴...")
    }
    runBlocking {
        job.join()
    }
    println("메인쓰레드 종료")
}

ㄴ> 위 코드는 GlobalScope 로 코루틴을 생성했고 launch를 통해 실행한다. GlobalScope는 전역 범위에서 바로 코루틴을 시작하므로 별도의 스코프 생성이 필요하지 않다. (어플리케이션 전체에서 코루틴이 실행되며 생명주기 또한 어플리케이션을 따라가기 때문에 명시적으로 cancel 하지 않아도 됨)

fun main(args: Array<String>) {
    println("메인쓰레드 시작")
    var job = CoroutineScope(Dispatchers.Default).launch {
        delay(3000)
        println("여기는 코루틴...")
    }
    runBlocking {
        job.join()
    }
    println("메인쓰레드 종료")
    job.cancel() // CoroutineScope: 따로 취소 처리가 필요함
}

ㄴ> 위 코드는 CoroutineScope 를 생성하여 해당 스코프에서 launch 함수를 사용해 새로운 코루틴을 시작한다. 또한 이 코루틴은 Dispathcher.Default 쓰레드 풀에서 실행된다. (CoroutineScope는 필요할 때 생성하고 꼭 cancel을 명시적으로 호출해야 한다. 왜냐하면 리소스 누수를 방지해야 하기 때문)

 


< 정리 >

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