상세 컨텐츠

본문 제목

Kotlin Coroutine - (1-3) 코틀린에서의 동시성

안드로이드

by sorryMan 2022. 2. 15. 23:00

본문

다른언어에서와 달리 코틀린에서의 동시성을 살펴보자.


Non Blocking

스레드는 무겁고, 생성하는데 비용을 많이 쓰며, 제한된 수의 스레드만 생성할 수 있다. 따라서 스레드가 블로킹되면 자원이 낭비되는 셈이여서 코틀린은 중단 가능한 연산(Suspendable Computations)이라는 기능을 제공한다. 
스레드의 실행을 블로킹하지 않으면서 실행을 잠시 중단하는것이다.
예로 작업이 끝나기를 기다리는 스레드를 블로킹하는 대신, 대기해야하는 코드를 일시 중단하고, 그동안 다른 연산 작업에 이용하는것이다.

또한 코틀린은 채널, 액터, 상호 배제(mutual exclusions)와 같은 훌륭한 기본형(primitives)도 제공해 스레드를 블록하지 않고 동시성 코즈를 효과적으로 통신하고 동기화하는 매커니즘을 제공한다.

명시적 선언

동시성은 연산이 동시에 실행돼야하는 시점을 명시적으로 선언해주는것이 중요하다. (안그러면 race condition에 빠지기 쉽다.)
Suspendable computations는 기본적으로 순차적으로 실행되게된다. 이때 스레드를 블로킹 하지않기 때문에 직접적인 단점은 아니다.

fun main() = runBolcking {
    val name = getName()
    val lastName = getLastName()
    println("$name, $lastName") //시간은 2000ms이상이 걸린다.
}

//suspend를 통해 일시중단이 가능하게 한다.
suspend fun getName(): String {
	delay(1000)
    return "Link"
}

suspend fun getLastName(): String {
	delay(1000)
    return "JS"
}

위 코드에서 main에서 getName()와 getLastName()은 순차적으로 실행한다.
이렇게 실행 스레드를 블록하지않고, 비동시성 코드를 작성할 수 있어서 편리하다.
하지만 getName()과 getLastName()이 연관이 없다라는걸 알게되면, 동시에 실행시키는게 속도가 빠르다.

fun main() = runBolcking {
    val name = async{ getName() }
    val lastName = async { getLastName()}
    println("${name.await()}, ${lastName.await()}") //시간은 1000ms이상이 걸린다.
}

위처럼 코드를 바꿔 동시에 실행시키고, await()를 통해 두 값이 도착할때까지 main을 일시 중단 시켰다.

가독성

코틀린의 동시성코드는 순차적 코드만큼 읽기 쉽다.
자바를 비롯해 다른 언어에서는 동시성 코드를 읽고 이해하고 디버깅하는게 어려운 문제가 있는데, 코틀린의 접근법은 관용구적인 동시성 코드를 허용한다.

suspend fun getProfile(id: Int) {
    val basicUserInfo = asyncGetUserInfo(id)
    val contactInfo = asyncGetContactInfo(id)
    
    createProfile(basicUserInfo.await(), contactInfo.await())
}

suspend 메소드는 두 async메소드를 실행하고, 둘다 완료가 될때까지 기다린다. 위 코드를 보면 순차적으로 읽어도 잘읽히는 디버깅하기 쉬운 코드가 됐다.

유연성과 기본형의 활용

스레드를 만들고 관리하는것은 동시성 코드를 작성할때 가장 어려운 부분중 하나이다.
언제 스레드를 만들 것인가 만큼 얼마나 스레드를 만드는지 아는것도 중요하다. 또한 I/O 작업 스레드와 CPU바운드 작업을 처리하는 스레드가 있어야하는데, 스레드를 통신/동기화 하는것은 그자체로도 어렵다.

하지만 우리의 코틀린은 간단하면서도 유연하게 동시성을 사용하게 해주는 고급함수와 기본형을 제공하고 있다.

- 스레드이름을 파리미터로 하는 newSingleThreadContext()를 호출하면 스레드가 생성된다. 일단 생성해두면 코루틴을 수행하는데 사용할 수 있다.

- 스레드 크기와 이름을 파라미터로하는 newFixedThreadPoolContext()를 호출하면 스레드 풀을 생성할 수 있다.

- CommonPool은 CPU바운드 작업에 최적인 스레드 풀이다. 최대 크기는 시스템 코어에서 1을 뺀 값.

- 코루틴을 다른 스레드로 이동시키는 역할은 런타임이 담당한다.

- 채널, 뮤텍스 및 스레드 한정과 같은 코루틴의 통신과 동기화를 위해 필요한 많은 기본형과 기술이 제공된다. 

더보기

채널 (Channel)  : 코루틴 간에 데이터를 안전하게 보내고 받는데 사용하는 파이프

작업자 풀 (Worker Pool) : 많은 스레드에서 연산 집합의 처리를 나눌 수 있는 코루틴의 풀

액터 (Actor) : 채널과 코루틴을 사용하는 상태를 감싼 래퍼, 상태를 안전하게 수정하는 매커니즘을 제공

뮤텍스 (Mutexes) : 한번에 하나의 스레드만 실행 할 수 있도록 하는 동기화 매커니즘, 다른 스레드가 접근하려면 대기해야한다.

스레드 한정 (Thread confinement) : 코루틴의 실행을 제한해서 지정된 스레드에서만 실행하도록 하는 기능

 

코틀린 동시성 관련 개념과 용어

일시 중단 연산 (suspending computations)

해당 스레드를 차단하지않고 실행을 일시 중단 할 수 있는 연산.
스레드를 일시 중단하고 다시 시작해야 할 때까지 스레드를 다른 연산에서 사용할 수있다.

일시 중단 함수

함수형식의 일시중단 연산이다. 위에 코드에서 함수 앞에 suspend가 붙은걸로 쉽게 확인 할 수있다.

예시로 suspend function 내부에 delay를 사용했다면, delay()동안 일시중단된다. 따라서 delay 자체도 일시 중단 함수이며, delay()를 통해 일시 중지된 동안, 실행 스레드는 다른 연산을 수행하는데 사용 될 수 있다.

람다 일시 중단

일반적인 람다와 마찬가지로, 일시 중단 람다는 익명의 로컬 함수다.
일반적인 람다와 차이로는 다른 일시 중단 함수를 호출함으로써 자신의 실행을 중단할 수 있다는 점에서 다르다.

코루틴 디스패처 (Coroutine Dispatcher)

코루틴을 시작하거나 재개할 스레드를 결정하기 위해 코루틴 디스패처가 사용된다.
모든 코루틴 디스패처는 CoroutineDispatcher 인터페이스를 구현해야한다.

코루틴 빌더

일시 중단 람다를 받아, 그것을 실행시키는 코루틴을 생성하는 함수를 말한다.
코루틴은 아래에 다양한 일반적인 시나리오에 맞는 코루틴 빌더를 제공한다.

- async(): 결과가 예상되는 코루틴을 시작하는데 사용한다. 하지만 코루틴 내부에서 일어나는 모든 예외를 캡처해서 결과에 넣기때문에 조심해서 써야한다. Deferred<T>를 반환한다.
- launch(): 결과를 반환하지 않는 코루틴을 시작한다. 코루틴의 실행을 취소하기 위한 Job을 반환한다.
- runBlocking(): 블로킹 코드를 일시 중지 가능한 코드로 연결하기 위해 작성됐다. 보통 main()메소드와 유닛테스트에서 사용한다. 코루틴의 실행이 끝날때까지 현재 스레드를 차단한다.


이렇게 1장에서 동시성에 대해서 알아보았다.

반응형

관련글 더보기

댓글 영역