목차
코틀린은 코루틴이라는 기능을 통해 동시성을 지원합니다. 블로킹이 발생하는 일부 작업을 논블로킹화 시켜 시스템의 리소스를 효율적으로 사용하는데에 큰 도움을 줍니다.
다른 언어에도 이와 같은 이름을 가진 기능을 지원하고 있고 자바에서는 concurrent 패키지에 있는 Thread 클래스를 좀 더 쓰기 쉽게 래핑한 ExecutorService 같은 클래스 등을 통해 기본적인 비동기를 지원하고 reactive programming의 패러다임을 통해 비동기 처리를 위한 RxJava, Project Reactor 등의 리액티브 라이브러리를 통해서도 비동기 처리가 가능합니다.
Coroutine
코루틴은 일반적인 함수와 다르게 진입점이 여러군데 입니다. 함수는 한번 실행되면 끝날때까지 계속 진행된다면 코루틴은 중간에 잠시 멈췄다 다시 실행이 가능합니다. 이 과정에서 기존에 실행하는 상태를 저장하고 추후 실행포인트에 복귀했을 때 기존 상태를 가지고 다시 진행이 가능하다는 특징이 있습니다.
코틀린에서 코루틴은 일급 객체입니다. 기본적으로 코루틴을 지원하지만 실제로 사용하기 위해서는 별도의 라이브러리를 통해 정의된 편의함수를 사용해야 합니다.
dependency
코루틴을 사용하기 위해서는 위에서 언급한대로 별도의 라이브러리가 필요합니다. 메이븐이나 그래들을 통해 의존성을 추가 할 수 있습니다.
코루틴은 JVM, native 환경에서만 사용이 가능합니다.
1 | dependencies { |
예제
1 | import kotlinx.coroutines.delay |
실행결과는 아래와 같습니다. (물론 랜덤으로 지연되게 했기 때문에, 결과는 실행마다 다릅니다.)
1 | 2021-11-30T02:02:44.146 - run both task on thread Thread[main,5,main] |
일반적인 함수였다면 task1이 실행 / 완료 후 task2 가 실행 / 완료 되는 순서로 실행됐겠지만 코루틴을 사용하여 위와 같은 결과를 만들 수 있었습니다.
재미있는 점은 delay
함수가 실행될 때 sleep
처럼 스레드 전체가 멈추는 것이 아닌 다른 코루틴이 실행된다는 점입니다.
각 함수 및 키워드에 대해 간단히 설명하면
runBlocking
함수는 람다와 코루틴 컨텍스트 객체를 받는 함수로 기존의 블로킹 연산을 코루틴으로 감싸기 위해 사용하는 함수입니다.launch
는 파라메터로 받은 람다를 별도의 코루틴에서 실행하게 도와주는 함수입니다.- task는
suspend
fun 형태를 가지는데,suspend
함수는 코루틴 내부에서 일반 함수처럼 사용됩니다. 다만 내부에서 delay 나 yield 함수를 통해 실행을 지연시킬 수 있습니다. delay
함수는 코루틴 내부에서 지연 실행이 필요할 때 사용하는 함수입니다. 실제 스레드를 멈추지 않고 현재 실행상태를 멈추고 다른 코루틴이 실행될 수 있도록 한 후 delay 한 시간이 지나면 이후 문장이 실행되게 합니다. 위 예제에서는 0.5초 혹은 1초 뒤에 다음 문장이 실행되도록 지연해주는 역할을 합니다.
만약 delay
대신 sleep을
사용했다면 task1 -> task2 -> ‘run both’ 메시지 표시 순으로 이루어졌을 것입니다. 로그에서 알 수 있듯이 모든 실행 스레드가 메인스레드라는 점에서 delay
과정에서는 스레드를 점유하여 물리적인 지연을 시키는 것이 아닌 내부 스케쥴러에 의해 지연된 실행을 할 수 있도록 했다는 것을 알 수 있습니다.
coroutine context / thread
위 예제에서는 모든 코루틴들이 메인 스레드에서 실행되는 것을 알 수 있었습니다. 코루틴에서는 별도의 컨텍스트를 지정하지 않았다면 기본 컨텍스트 상에서 동작합니다. 완벽한 비동기를 위해서는 여러 스레드를 통해 실행될 필요가 있습니다. 이를 위해 코루틴은 runBlocking
, launch
등의 함수에 컨텍스트를 명시적으로 선언하여 원하는 컨텍스트(그리고 스레드)에서 코루틴을 실행 시킬 수 있습니다.
코루틴 라이브러리에는 여러 일반적인 상황을 위해 미리 정의된 코루틴 컨텍스트가 있습니다.
Dispatchers.Default
max(cpu 코어수, 2) 의 수 만큼 스레드풀을 만들어 사용하는 컨텍스트 입니다. 보통 cpu bound 잡을 실행할 때 사용합니다.Dispatchers.IO
스레드 풀내에 가용 스레드가 부족하면 스레드를 늘리는 컨텍스트입니다. 이름 그대로 IO 작업을 할 때 사용합니다.Dispatchers.Main
안드로이드 / swing 어플리케이션에서 메인스레드를 가리키는 컨텍스트입니다. 두 어플리케이션의 UI 업데이트 시에 사용합니다.
위 컨텍스트를 이용해 1번 코루틴을 다른 스레드에서 실행하도록 해봤습니다.
1 | import kotlinx.coroutines.* |
실행결과는 다음과 같습니다.
1 | 2021-11-30T02:34:22.123 [main] - run both task |
task1이 다른 잡들과 다른 스레드에서 병렬로 실행되는 것을 알 수 있습니다.
커스텀 스레드 풀 사용하기
본인이 작성했거나 기본적으로 적용된 스레드가 아닌 별도의 커스텀 풀을 사용하고 싶을 경우 코루틴 라이브러리에 정의된 확장함수 asCoroutineDispatcher()
를 이용하여 변환이 가능합니다.
1 | Executors.newWorkStealingPool().asCoroutineDispatcher().use { ctx -> |
use는 Closable 인터페이스의 확장함수로 자바의 try-with-resource 와 같은 기능을 합니다.
CoroutineStart
launch
함수에는 context 외에 CoroutineStart
파라메터를 추가로 받습니다. 이름 그대로 코루틴 시작 정책에 대한 내용으로 총 4가지 값을 가집니다.
Default
기본값으로 launch 함수의 실행과 동시에 람다를 컨텍스트 내의 스레드에서 실행합니다.Lazy
명시적으로start
를 호출해야 람다가 실행됩니다.ATOMIC
중간에 스레드 외부로 부터 interrupt 가 불가능 한 실행입니다.UNDISPATCHED
중단이 발생할 때(delay, yield 같은 함수가 실행될 때) 부터 컨텍스트 내의 스레드로 이관되는 형태를 가집니다.
비동기로 결과 받기
launch
함수는 Job이라는 객체를 리턴합니다. Future 와 비슷한 객체로 launch
의 람다가 실행이 끝날 때 까지 대기를 도와주는 객체입니다. 만약 람다의 결과값을 사용하고 싶다면 async
라는 함수를 사용합니다. async
는 Deferred 라는 객체를 리턴하는데, Job에 결과값을 별도로 담을 수 있습니다.
1 | fun main() { |
1 | 2021-12-02T03:21:15.44 [DefaultDispatcher-worker-1] - processing |
Deferred
의 await
함수로 결과값을 받을 수 있는데, 결과를 받기위해 대기할 때도 스레드를 블로킹하지 않습니다.
continuation
suspend 함수는 데이터를 리턴하고 생긴것은 일반함수와 비슷하지만 앞서 살펴본것과 마찬가지로 실행 중간에 컨텍스트를 다른 코루틴으로 넘기는 등의 형태로 중단하고 다시 컨텍스트로 복귀하여 작업을 지속합니다. 이는 Continuation이라는 인터페이스 덕분에 가능합니다. 이 인터페이스는 함수가 suspension 된 이후에 돌아올 실행 포인트를 나타냅니다.
인터페이스의 resumeWith 라는 메소드를 통해 다른 컨텍스트에서 실행된 결과를 반환받아 리턴할 수 있게 해줍니다.
출처
다재다능 코틀린 프로그래밍
코틀린 공식문서