쟈미로그

코루틴 입문 - 1. 코루틴 기초 본문

Kotlin

코루틴 입문 - 1. 코루틴 기초

쟈미 2023. 9. 10. 22:11

코루틴.. 공부해야지 마음먹었는데 마침 인프런 최태현 강사님이 코루틴 입문 강의를 올려주셨다;; 강의 ->-> 2시간으로 끝내는 코틀린.

운명이다 싶어서 홀린듯 결제했다.

 

섹션 1. 코루틴 기초를 수강 후 정리하는 글!

 

 

1강. 루틴과 코루틴

코루틴? (co-routine) : 협력하는 루틴(함수).

 

루틴

그렇다면 그냥 루틴(함수)은 뭘까? 그냥 루틴도 협력을한다.

먼저 루틴을 알아보자.

fun main() {
	println("START")
	newRoution()
	println("END")
}

fun newRountine() {
	val num1 = 1
	val num2 = 2
	println("${num1 + num2}")
}

여기서 루틴은 main과 newRoutine 2개다. main은 newRoutine을 부르고 결과를 받는 등 협력하고 있다.

이 코드의 결과는 직관적이다.

START
3
END

 

  • 코드 실행 관점
  1. main 루틴이 START 출력 후 newRoutine 호출
  2. newRoutine은 지역변수 num1, num2 초기화 후 합한 값 3을 출력
  3. newRoutine 종료 후 main 루틴으로 돌아옴
  4. main 루틴은 END 출력 후 종료

루틴 동작 과정

  • 메모리 관점

newRoutine이 호출되면, newRoutine이 사용하는 스택에 지역변수 num1, num2가 초기화됨

-> newRoutine이 끝나면 해당 스택에 접근할 수 없어짐. 

 

  • 정리
  1. 루틴은 진입하는 곳이 1곳
  2. 루틴이 종료되면 그 루틴에서 사용했던 정보가 초기화됨

 

코루틴

fun main(): Unit = runBlocking {
    println("START")

    launch {
        newRoutine()
    }
    yield() 

    println("END")
}

suspend fun newRoutine() {
    val num1 = 1
    val num2 = 2
    yield()
    println("${num1 + num2}")
}

먼저 새로운 키워드/메소드들을 간단하게 알아보자.

  • runBlocking : 일반 루틴 세계-코루틴 세계를 연결하는 함수. 이 함수 선언 자체로 새로운 코루틴을 만듦.
  • launch : 새로운 코루틴을 만드는 함수. 주로 반환값이 없는 코루틴을 만드는데 사용됨.

즉, 위 코드는 runBlocking과 launch로 2개의 코루틴을 만든 것이다.

  • yield : 현재 코루틴의 실행을 잠시 멈추고 다른 코루틴이 실행되도록 양보함.
  • suspend fun : suspend 키워드를 붙이면 다른 suspend fun을 호출할 수 있게 됨. yield()가 suspend fun이라서 newRoutine 메소드는 suspend fun으로 선언됨.

 

이렇게 기본적인 코드 의미를 알아봤으니, 위 코드의 결과를 확인해보자.

START
END
3

일반 루틴을 사용했을 때와 결과가 다르다.. 왜 이런 결과가 나왔는지 알아보자.

(yield() 없어도 결과는 동일함)

 

  • 코드 실행 관점
  1. runBlocking에 의해서 main 코루틴이 시작되고, START 출력
  2. launch에 의해서 새 코루틴이 생김. 그러나, newRoutine 실행은 바로 일어나지 않고 현재 main 코루틴이 진행됨
  3. main 코루틴 안에 yield()가 되면 main 코루틴은 새 코루틴에게 실행을 양보함. 따라서 새 코루틴이 실행되고, newRoutine 메소드를 실행함
  4. newRoutine 메소드의 yield()에 의해 다시 main 코루틴으로 돌아옴
  5. main 루틴은 END 출력 후 종료
  6. 새 코루틴 차례가 되어 newRoutine 메소드로 돌아와서, 3 출력 후 종료

코루틴 동작 과정

  • 메모리 관점

newRoutine의 지역변수인 num1, num2는 새로운 코루틴이 완전히 종료되기 전까지 메모리에서 제거되지 않는다.

 

  • 정리

즉, 루틴-코루틴의 가장 큰 차이는 중단재개다.

루틴은 한번 시작되면 종료될 때까지 멈추지 않지만, 코루틴은 상황에 따라 잠시 중단됐다가 다시 시작될 수 있다!

 


2강. 스레드와 코루틴

스레드-코루틴 차이

코루틴은 스레드와 자주 비교된다. 또한 스레드는 프로세스와 자주 거론된다. 면접 단골 질문,,

코루틴-스레드-프로세스를 비교해보면서 코루틴을 이해해보자!

 

프로세스-스레드-코루틴

  • 프로세스(process) : 컴퓨터에서 실행되고 있는 프로그램
  • 스레드(thread) : 프로세스에 소속되어, 여러 코드를 동시에 실행할 수 있도록 해줌. 코드를 실행하는 주체
  • 코루틴 : 단지 우리가 작성한 루틴/코드 종류 중 하나. 코루틴 코드가 실행되려면 스레드가 있어야함.

그렇다면 일반적인 코드(루틴)와 다른점은, 코루틴은 중단-재개가 가능하므로, 코루틴 코드의 앞부분은 1번 스레드에 배정되고 뒷부분은 2번 스레드에 배정되는 것이 가능하다!!

 

그렇다면 코루틴은 어떤 차별화된 특징, 장점을 가질까?

 

Context Switching

  • 프로세스
    • 프로세스들은 각자의 독립된 메모리 영역을 가짐. 때문에 컨텍스트 스위칭 발생 시 힙 역역+스택 영역이 모두 교체되어야함
    • 즉, 컨텍스트 스위칭 비용 가장 큼
  • 스레드
    • 스레드들은 독립된 스택 영역과 공유하는 힙 영역을 가짐. 때문에 컨텍스트 스위칭 발생 시 스택 영역만 교체됨
    • 즉, 프로세스보단 컨텍스트 스위칭 비용 적음
  • 코루틴
    • 반면 코루틴은 여러 코루틴이 한 스레드에서 실행될 수 있음
    • 동일 스레드에서 코루틴이 실행되면, 메모리 전부를 공유하므로 스레드보다 컨텍스트 스위칭 비용지 적음

 

동시성 (consistency)

동시성 : 여러 작업이 번갈아가면서 아주 빠르게 실행되어, 마치 동시에 실행되고 있는 것처럼 보이는 것.
병렬성 : CPU 코어가 여러개 있어서, 실제로 2가지 이상의 일을 동시에 하는 것
  • 스레드 : 동시성 확보를 위해 2개 이상의 스레드가 필요하다.
  • 코루틴 : 한 스레드에서 여러 코루틴이 번갈아서 실행될 수 있기 때문에 1개 스레드만으로도 동시성 확보가 가능하다!

 

비선점형

  • 스레드 : 스레드는 다른 스레드를 실행되도록 하는 주체가 OS임. 이를 '선점형'이라 함
  • 코루틴 : yield()처럼, 코루틴 스스로 다른 코루틴에게 실행을 양보할 수 있음. 이를 '비선점형'이라 함

3강. 코루틴 빌더와 Job

3강에서는 코틀린에서 코루틴 만드는 방법과, 각 방법들의 차이점을 알아본다.

코루틴을 만드는 함수를 코루틴 빌더라 한다. 코틀린의 코루틴 빌더에는 1. runBlocking, 2. launch, 3. async가 있다.

 

(1) runBlocking

runBlocking은 새 코루틴을 만들고, 루틴 세계-코루틴 세계를 이어주는 역할을 한다.

 

주의점 - Blocking

이 함수의 주의점은 Blocking이라는 점이다!

runBlocking은 내부에서 생성된 코루틴들이 모두 완료될 때까지 스레드를 블락시킨다.

fun main() {
    runBlocking {
        printWithThread("START")
        launch {
            delay(2_000L)
            printWithThread("LAUNCH END")
        }
    }
    
    printWithThread("END")
}

예시로, 이 코드의 결과는 아래와 같다.

START
LAUNCH END // 2초 지연 후 출력됨
END

즉, runBlocking 밖의 END 출력까지 스레드는 아무 의미 없이 2초를 기다려야된다. 이렇게 runBlocking을 잘못 사용하면 스레드가 블락돼서 다른 코드가 실행될 수 없다.

 

사용하는 경우

때문에 runBlocking은 계속해서 사용하는 함수는 아니다.

  1. 프로그램에 진입하는 최초의 메인 함수
  2. 테스트 코드 시작 시

사용하기에 좋다.

 

 

(2) launch

반환값이 없는 코드를 실행할 때 주로 사용하는 코루틴 빌더다.

 

runBlocking과 다르게 launch는 생성된 코루틴 객체를 결과로 반환한다. (코드 블럭 내의 반환값 X 생성된 코루틴 객체 O) 이 객체를 이용해서 코루틴을 제어할 수 있다.

객체 타입은 Job이다.

 

 

코루틴의 제어?

(1) 시작 제어 - start()

start 메소드로 코루틴의 시작 시점을 제어해보자.

fun main(): Unit = runBlocking {
    val job = launch(start = CoroutineStart.LAZY) { // 1. LAZY 옵션으로 코루틴 즉시 실행을 막음
        printWithThread("Hello launch")
    }
    delay(1_000L)
    job.start() // 2. 코루틴 실행!
}

먼저

1. CoroutineStart.LAZY 옵션으로 코루틴 즉시 실행을 막았고,

2. 생성된 코루틴 객체를 이용해서 job.start()를 호출하여 코루틴을 시작시켰다. 

 

 

(2) 취소 제어 - cancel()

cancel 메소드로 코루틴을 취소해보자.

fun main(): Unit = runBlocking {
    val job = launch {  // 1. 0.5초 간격으로 1~5까지 출력하는 코루틴#2
        (1..5).forEach {
            printWithThread(it)
            delay(500)
        }
    }
    delay(1_000L)
    job.cancel() // 2. 1초 뒤 job을 취소시키는 코루틴#1
}

이 경우, 원래대로라면 0.5초 간격으로 1~5까지 출력될 수 있었지만, 1초 delay 후 job.cancel()로 해당 코루틴을 취소했기 때문에 1, 2만 출력된다.

 

 

(3) 대기 제어 - join()

join 메소드로 코루틴이 끝날 때까지 대기할 수 있다.

join 사용 안한 것과 사용한 예를 보면서 알아보자.

// 첫 번째 코드 - join 사용 안함
fun main(): Unit = runBlocking {
    val job1 = launch {
        delay(1_000)
        printWithThread("Job 1")
    }
    
    val job2 = launch {
        delay(1_000)
        printWithThread("Job 2")
    }
}

이 경우 각 코루틴에 delay 1초가 있음에도 Job1, Job2 출력까지 약 1.1초면 충분하다.

 

이 코드에서 join을 추가하면 어떻게 될까?

// 두 번째 코드 - join 사용
fun main(): Unit = runBlocking {
    val job1 = launch {
        delay(1_000)
        printWithThread("Job 1")
    }
    job1.join()
    
    val job2 = launch {
        delay(1_000)
        printWithThread("Job 2")
    }
}

이 경우 첫번째와 다르게 코드 수행에 약 2초가 걸린다. join을 이용해서 첫번째 launch 코루틴이 끝날 때까지 대기했기 때문이다.

Comments