쟈미로그
코루틴 입문 - 1. 코루틴 기초 본문
코루틴.. 공부해야지 마음먹었는데 마침 인프런 최태현 강사님이 코루틴 입문 강의를 올려주셨다;; 강의 ->-> 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
- 코드 실행 관점
- main 루틴이 START 출력 후 newRoutine 호출
- newRoutine은 지역변수 num1, num2 초기화 후 합한 값 3을 출력
- newRoutine 종료 후 main 루틴으로 돌아옴
- main 루틴은 END 출력 후 종료
- 메모리 관점
newRoutine이 호출되면, newRoutine이 사용하는 스택에 지역변수 num1, num2가 초기화됨
-> newRoutine이 끝나면 해당 스택에 접근할 수 없어짐.
- 정리
- 루틴은 진입하는 곳이 1곳
- 루틴이 종료되면 그 루틴에서 사용했던 정보가 초기화됨
코루틴
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() 없어도 결과는 동일함)
- 코드 실행 관점
- runBlocking에 의해서 main 코루틴이 시작되고, START 출력
- launch에 의해서 새 코루틴이 생김. 그러나, newRoutine 실행은 바로 일어나지 않고 현재 main 코루틴이 진행됨
- main 코루틴 안에 yield()가 되면 main 코루틴은 새 코루틴에게 실행을 양보함. 따라서 새 코루틴이 실행되고, newRoutine 메소드를 실행함
- newRoutine 메소드의 yield()에 의해 다시 main 코루틴으로 돌아옴
- main 루틴은 END 출력 후 종료
- 새 코루틴 차례가 되어 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은 계속해서 사용하는 함수는 아니다.
- 프로그램에 진입하는 최초의 메인 함수
- 테스트 코드 시작 시
사용하기에 좋다.
(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 코루틴이 끝날 때까지 대기했기 때문이다.
'Kotlin' 카테고리의 다른 글
[Kotlin] 자바 개발자를 위한 코틀린 입문 - 2 (0) | 2023.07.16 |
---|---|
[Kotlin] 자바 개발자를 위한 코틀린 입문 - 1 (0) | 2023.07.02 |