일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 계측
- Flutter
- Compose
- appbar
- LifeCycle
- Button
- Kotlin
- DART
- Navigation
- Coroutines
- textview
- ScrollView
- binding
- 앱
- intent
- 안드로이드
- viewmodel
- TEST
- textfield
- 테스트
- activity
- scroll
- android
- tabbar
- data
- drift
- livedata
- Dialog
- CustomScrollView
- 앱바
- Today
- Total
Study Record
[Kotlin] coroutines 살펴보기 본문
😶 Thread 와 Android 개요
앱을 만들면서 서버와의 통신(네트워킹), 데이터베이스 작업과 같은 고급 기능을 추가하면서 작업 수행 시간이 길어지면 앱의 UI가 끊겨서 보이거나 버튼을 클릭했을 때 실행되지 않은 것처럼 보이기도 한다.
작업 수행이 길면 왜 이런 문제점이 생기느냐를 논하기에 앞서, 스레드의 개념을 알고 있어야 한다. 스레드는 프로그램이 실행되는 가장 작은 코드 단위이며 단순하게 스레드를 앱에서 코드를 실행하는 단일 경로라고 생각할 수 있다. 코드 한 줄이 실행이 끝나야 다음줄로 넘어갈 수 있다. 같은 블록에 작성된 코드는 동일한 스레드에서 순차적으로 실행된다.
모든 앱은 메인 스레드(UI 스레드)가 하나씩 있고 개발자가 코드로 스레드를 생성/시작시킬 수 있다. 이렇게 실행 중인 앱에는 여러 스레드가 있을 수 있는데 메인 스레드가 앱의 UI 와 관련된 작업을 담당하기 때문에 메인 스레드에 네트워킹, 데이터베이스 작업과 같은 시간이 오래 걸리는 작업을 수행하면 도중에 다른 코드를 수행할 수 없으므로 사용자의 UI 응답과 같은 작업이 바로 실행되지 못하여 앱이 응답하지 않게 된다.
😶 Thread and unpredictable behavior
Kotlin 에서 스레드를 실행하는 방법 중 하나로 Thread 클래스가 있다.
fun main() {
val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
repeat(3) {
Thread {
println("${Thread.currentThread()} has started")
for (i in states) {
println("${Thread.currentThread()} - $i")
Thread.sleep(50)
}
}.start()
}
}
여러 스레드를 실행했을 때, 예측할 수 없는 결과를 얻을 수 있는데 다음과 같은 코드를 실행하면,
fun main() {
var count = 0
for (i in 1..50) {
Thread {
count += 1
println("Thread: $i count: $count")
}.start()
}
}
Thread: 50 count: 49
Thread: 49 count: 50
Thread: 1 count: 1
Thread: 2 count: 2
Thread: 3 count: 3
Thread: 4 count: 4
Thread: 5 count: 5
Thread: 6 count: 6
Thread: 7 count: 7
Thread: 8 count: 8
Thread: 9 count: 9
Thread: 10 count: 10
Thread: 11 count: 11
Thread: 12 count: 13
Thread: 13 count: 13
Thread: 14 count: 14
Thread: 15 count: 15
Thread: 16 count: 16
Thread: 17 count: 17
Thread: 18 count: 18
Thread: 20 count: 19
Thread: 19 count: 20
결과를 보면, Thread: 12 count 13 과 같이 출력된 것이나 Thread: 60 count: 49 가 제일 먼저 출력된 것처럼 버그로 보이는 것들이 있다. 이것은 여러 스레드가 동시에 메모리의 동일한 값에 액세스 하려고 할 때 생기는 버그이다. 버그를 발견하는 것도 재현하는 것도 어렵기 때문에 스레드를 직접 사용하는 것을 권장하지 않는 이유 중 하나이다.
Kotlin 에서는 스레드의 이런 동시성 문제를 관리/유지하는 데 유용한 Coroutines 를 제공하고 있다.
😶 Coroutines
coroutine(코루틴)은 중단 가능한 계산 작업에 대한 인스턴스이다. 코루틴은 스레드와 비슷하지만 특정 스레드에 묶여 있지 않는다. 하나의 스레드에서 실행을 중단에서 다른 스레드에서 코루틴을 다시 시작할 수 있다.
코루틴 예시)
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
/* 실행 결과
Hello
World!
*/
launch{} 는 coroutine builder 로 새로운 코루틴을 할 수 있는데 launch{} 외 나머지 작업과 독립적으로 실행된다. 따라서, 1초 뒤에 World! 가 찍히기 전, Hello 가 먼저 실행결과에 찍힌다.
😶 Coroutines 기본 개념
coroutines 는 구조적 동시성(structured concurrency)의 원칙을 따른다. 이 의미는 새로운 코루틴은 항상 특정 CoroutineScope 에서만 시작할 수 있다. 이 글에서 소개할 CoroutineScope 를 정의할 수 있는 메서드는 runBlocking, coroutineScope, MainScope 가 있다. MainScope 는 밑에서 설명할 예정이다.
runBlocking
runBlocking 메서드를 실행하는 스레드(ex. 메인 스레드)는 runBlocking 에 명시된 작업이 끝날 때까지 Block 된다. 일반 함수와 비슷하다. 멀티태스킹을 기대하는 것이라면 이 메서드는 사용하면 안 된다.
fun main() { // this: CoroutineScope
runBlocking {
delay(1000)
println("Hello")
}
println("END")
}
/* 실행 결과
Hello
END
*/
coroutineScope
runBlocking 과 비슷하지만 코루틴 혹은 suspending function 에서만 호출할 수 있는 메서드이다. runBlocking 메서드는 현재 메서드를 차단(block)하지만 coroutineScope 는 일시 중단한다.(suspend)
// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
doWorld()
println("Done")
}
// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(2000L)
println("World 2")
}
launch {
delay(1000L)
println("World 1")
}
println("Hello")
}
/* 실행 결과
Hello
World 1
World 2
Done
*/
coroutineScope 이기 때문에 실행이 끝날 때까지 일시중단된다. 따라서, Hello, World1, World2 가 모두 출력되고 Done 이 마지막에 출력된다.
😶 Suspending Function
함수 앞에 suspend 키워드를 붙이면 Suspending Function 이 된다. Suspending Function 은 코루틴 안에서 호출될 수 있으며 이점으로, 안에 delay() 메서드와 같은 suspending function 을 호출할 수 있다.
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
// this is suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
/* 실행 결과
Hello
World!
*/
😶 새로운 코루틴 시작하기(launch)
새로운 코루틴을 실행하는 coroutine builder 로는 launch 가 있다. 리턴값으로 Job 개체를 리턴하는데 이 개체로 코루틴을 관리할 수 있다.
fun main() {
runBlocking {
val job = launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello")
job.join() // wait until child coroutine completes
println("Done")
}
println("END")
}
/* 실행 결과
Hello
World!
Done
END
*/
😶 Async 와 Suspending Function
한 코루틴 내에서 실행하는 코드들은 순차적으로 실행된다. 코드 한 줄의 실행시간이 얼마나 오래 걸려도 실행을 마치고 다음 줄을 실행한다. 다음 예시에서의 코드에서 doSomethingUsefulOne() 함수가 실행된 다음 doSomethingUsefulTwo() 함수가 실행되고 나머지 작업이 실행된다.
fun main() = runBlocking{
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
println("doSomethingUsefulOne's delay is done")
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
println("doSomethingUsefulTwo's delay is done")
return 29
}
/* 실행 결과
doSomethingUsefulOne's delay is done
doSomethingUsefulTwo's delay is done
The answer is 42
Completed in 2040 ms
*/
이렇게 순차적으로 실행하는 것보다 doSomethingUsefulOne() 함수와 doSomethingUsefulTwo() 함수를 각각 실행한 뒤, 두 함수의 작업이 끝나면 나머지 작업을 실행할 수 있으면 더 효율적일 것이다. 마치, 심부름을 할 때 나와 동생이 같이 마트에 갔다가 빵집에 가는 것보다 내가 마트에 갈 동안 동생이 빵집에 간 뒤 각각 볼일을 마치고 집으로 돌아오는 것이 더 빠른 것과 비슷하다.
이럴 때 사용할 수 있는 것은 async(비동기)이다. async 는 launch 와 마찬가지로 coroutine builder 이다. 단순히 작업을 분리하는 것이라면 새로운 코루틴을 생성하는 launch{} 를 사용해도 무방할 것이다. 하지만 launch{} 는 결괏값을 리턴 받을 수 없다. 반면에 async 를 사용하면 리턴 받을 값이 있다면 .await() 함수로 리턴 받을 수 있다.
fun main() = runBlocking{
val time = measureTimeMillis {
val one = async {doSomethingUsefulOne()}
val two = async {doSomethingUsefulTwo()}
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
println("doSomethingUsefulOne's delay is done")
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
println("doSomethingUsefulTwo's delay is done")
return 29
}
/* 실행 결과
doSomethingUsefulOne's delay is done
doSomethingUsefulTwo's delay is done
The answer is 42
Completed in 1096 ms
*/
늦게 시작하기
async 를 사용할 때 CoroutineStart.LAZY 파라미터를 주면 작업을 바로 시작하지 않고 .start() 함수로 시작할 수 있다.
fun main() = runBlocking{
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY) {doSomethingUsefulOne()}
val two = async(start = CoroutineStart.LAZY) {doSomethingUsefulTwo()}
two.start() // start the second one
one.start() // start the first one
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}
...
suspending function 과 async 함께 사용하기
async() 를 사용하려면 CoroutineScope 가 필요하다.
fun main() = runBlocking{
val time = measureTimeMillis {
println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")
}
suspend fun concurrentSum(): Int = coroutineScope {
val one = async {doSomethingUsefulOne()}
val two = async {doSomethingUsefulTwo()}
one.await() + two.await()
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L)
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L)
return 29
}
😶 CoroutineContext
코루틴을 실행하려면 항상 kotlin standrard library 에 정의된 CoroutineContext 가 필요하다. 따라서, coroutine cuilder 인 launch 함수와 async 함수 모두 CoroutineContext 가 필요하다.
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
coroutineContext 는 coroutine dispatcher 를 포함하고 있다. coroutine dispatcher 은 코루틴을 실행하는 데 사용되는 스레드(스레드들)를 결정한다. dispather 종류는 다음과 같다.
fun main(): Unit = runBlocking {
launch { // context of the parent, main runBlocking coroutine
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
delay(500)
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher
println("Default2 : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
}
/* 실행 결과
Unconfined : I'm working in thread main
Default : I'm working in thread DefaultDispatcher-worker-1
Default2 : I'm working in thread DefaultDispatcher-worker-2
main runBlocking : I'm working in thread main
newSingleThreadContext: I'm working in thread MyOwnThread
Unconfined : I'm working in thread kotlinx.coroutines.DefaultExecutor
*/
dispather 를 선언하지 않으면 부모로부터 종속받는다. runBlocking 은 메인 스레드에서 실행된다.
Dispatchers.Default 는 공유된 백그라운드 폴의 스레드폴이 사용된다.
Dispatchers.Unconfined 는 처음에는 호출한 스레드에 의해 실행되지만 delay() 가 호출된 후에는 새로운 스레드의 코루틴을 다시 시작한다. 특정 스레드에 제한된 공유 데이터를 업데이트하지 않는 코루틴에 적합하다.
newSingleThreadContext() 함수로 새로운 스레드를 직접 호출할 수 있지만 이 작업은 비용이 많이 드는 작업이다.
coroutine 상속과 Job
코루틴이 또다른 코루틴의 CoroutineScope 에 의해 시작될 때 부모의 CoroutineScope.coroutineContext 와 Job 이 상속된다. 따라서 새로운 코루틴은 부모 코루틴의 Job 의 자식이 되고 부모가 작업이 취소되면 자식도 같이 재귀적으로 취소된다.
하지만, GlobalScope.launch 에 의해 시작된 코루틴과 같이 시작할 때 다른 범위를 명시적으로 지정하면 상위 범뮈에서 Job 을 상속하지 않는다. 또한, Job 개체가 새 코루틴의 컨텍스트로 전달되면 상위 범위의 Job 을 재정의한다. 다음 예시는 코루틴이 생성될 때 Job 개체를 따로 선언한 예시이다.
fun main() = runBlocking<Unit> {
// launch a coroutine to process some kind of incoming request
val request = launch {
// it spawns two other jobs
launch(Job()) {
println("job1: I run in my own Job and execute independently!")
delay(1000)
println("job1: I am not affected by cancellation of the request")
}
// and the other inherits the parent context
launch {
delay(100)
println("job2: I am a child of the request coroutine")
delay(1000)
println("job2: I will not execute this line if my parent request is cancelled")
}
}
delay(500)
request.cancel() // cancel processing of the request
println("main: Who has survived request cancellation?")
delay(1000) // delay the main thread for a second to see what happens
}
/* 실행 결과
job1: I run in my own Job and execute independently!
job2: I am a child of the request coroutine
main: Who has survived request cancellation?
job1: I am not affected by cancellation of the request
*/
중간에 request.cancel() 함수에 의해 코루틴이 종료되어 자식 코루틴도 종료될 것이다. 하지만 launch(Job()) 으로 선언한 코루틴은 상속된 Job 이 아닌 재정의된 Job 을 가지로 있으므로 종료되지 않고 작업을 마치게 된다.
이름 붙이기
+ 기호를 사용하여 지정된 이름을 가진 코루틴을 사용할 수 있다.
fun main(): Unit {
runBlocking {
launch(Dispatchers.Default + CoroutineName("test")) {
println("I'm working in thread ${Thread.currentThread().name}")
}
}
}
/* 실행 결과
I'm working in thread DefaultDispatcher-worker-1 @test#2
*/
이름이 적용되는 것을 보려면 -Dkotlinx.coroutines.debug 옵션을 사용해야 한다.(JVM 옵션)
😶 Android 에서의 Coroutines
android 에서 코루틴을 사용한다고 하면, 제일 먼저 화면을 나타내는 Activity, Fragment 등을 떠올릴 것이다. Activity 나 Fragment 는 모두 수명주기를 가지고 있기 때문에 코루틴을 사용한다고 하면 Activity 나 Fragment 가 종료될 때 실행하고 있던 모든 코루틴을 종료해야 할 것이다.
예시) Activity 에서의 coroutines
class MyAndroidActivity {
private val scope = MainScope()
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
}
MainScope() 는 UI 컴포넌트들을 위한 주 CoroutineScope 를 생성한다.
예시) ViewModel 에서의 coroutines
class BusScheduleViewModel(private val scheduleDao: ScheduleDao) : ViewModel() {
...
fun getSchedule() {
// viewModelScope
viewModelScope.launch {
}
}
...
}
예시) Fragment 에서의 coroutines
class StopScheduleFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// coroutineScope
lifecycle.coroutineScope.launch {
}
}
}
'안드로이드 > Kotlin' 카테고리의 다른 글
[Kotlin] Enum Class (0) | 2023.08.10 |
---|---|
[Kotlin] Exception Class (예외 처리) (0) | 2023.08.09 |
[Kotlin] property (0) | 2023.07.21 |
[Kotlin] object (0) | 2023.07.20 |
[Kotlin] 람다식 및 고차함수 (0) | 2023.07.19 |