코루틴은 스레드와 기능적으로 같지만, 스레드에 비교하면 좀더 가볍고 유연하며 한단계 더 진화된 병렬 프로그래밍을 위한 기술입니다. 하나의 스레드 내에서 여러개의 코루틴이 실행되는 개념인데, 아래의 코드는 동일한 기능을 스레드와 코루틴으로 각각 구현한 코드의 예시입니다.
Thread(Runnable { for(i in 1..10) { Thread.sleep(1000L) print("I'm working in Thread.") } }).start() GlobalScope.launch() { repeat(10) { delay(1000L) print("I'm working in Coroutine.") } }
아래는 코루틴에 대해서 초점을 맞춰서 가장 간단한 코루틴의 예제입니다.
print("Start Main Thread") GlobalScope.launch { delay(3000) print("in Coroutine ...") } print("End Main Thread")
코루틴은 GlobalScope.launch로 정의되며 { .. } 으로 묶은 코드가 비동기적으로 실행됩니다. 실행 결과는 다음과 같습니다.
V: Start Main Thread V: End Main Thread V: in Coroutine ...
다음은 비동기적으로 실행된 코루틴이 완료되어 그 결과를 반환받는 예제입니다.
GlobalScope.launch { launch { print("At Here!") } val value: Int = async { var total = 0 for (i in 1..10) total += i total }.await() print("$value") }
결과는 다음과 같습니다.
V: At Here! V: 55
다음 코드 역시 비동기적으로 실행된 코루틴의 완료를 기다리고 그 결과를 반환받아 출력하는 예제입니다.
GlobalScope.launch { val x = doSomething() print("done something, $x") } private suspend fun doSomething():Int { val value: Int = GlobalScope.async(Dispatchers.IO) { var total = 0 for (i in 1..10) total += i print("do something in a suspend method: $total") total }.await() return value }
비동기적으로 실행되는 코루틴을 별도의 함수로 분리했는데, 코루틴 내부에서 실행되는 함수는 suspend로 지정해야 합니다. 위의 코드의 결과는 다음과 같습니다.
V: do something in a suspend method: 55 V: done something, 55
이번에는 2개의 코루틴을 실행하고 이 2개의 결과를 받아 출력하는 예제입니다.
print("Start...") GlobalScope.launch(Dispatchers.Main) { val job1 = async(Dispatchers.IO) { var total = 0 for (i in 1..10) { total += i delay(100) } print("job1") total } val job2 = async(Dispatchers.Main) { var total = 0 for (i in 1..10) { delay(100) total += i } print("job2") total } val result1 = job1.await() val result2 = job2.await() print("results are $result1 and $result2") } print("End.")
위의 코드에서 볼 수 있는 Dispatchers.Main, Dispatchers.IO는 각각 UI 변경 등을 처리하는 메인 스레드 그리고 입출력 연산을 처리하기에 적합한 IO 스레드를 의미하며, 코루틴들은 이처럼 지정된 스레드 내에서 실행됩니다. 위 코드의 결과는 다음과 같습니다.
V: Start... V: End. V: job1 V: job2 V: results are 55 and 55
다음은 코루틴이 완료를 기다리기 위한 await 호출을 사용하지 않는 또다른 방법입니다.
GlobalScope.launch(Dispatchers.IO) { val v = withContext(Dispatchers.Main) { var total = 0 for (i in 1..10) { delay(100) total += i } total } print("result: $v") print("Do something in IO thread") }
withContext를 써서 새로운 코루틴을 다른 스레드에서 동기적으로 실행하도록 하는 코드입니다. 결과는 다음과 같습니다.
V: result: 55 V: Do something in IO thread
launch는 Job 객체를 반환하는데, 이를 통해 다음 예제처럼 코루틴을 중간에 중단시킬 수 있습니다.
print("start..") val job = GlobalScope.launch() { repeat(10) { delay(1000L) print("I'm working.") } } runBlocking { delay(3000L) job.cancel() } print("stop")
실행 결과는 다음과 같습니다.
V: start.. V: I'm working. V: I'm working. V: stop
이번에는 Job 객체를 통해 코루틴이 완전이 종료될때까지 기다리는 예제입니다.
print("start..") val job = GlobalScope.launch() { repeat(10) { delay(1000L) print("I'm working.") } } runBlocking { job.join() } print("stop")
결과는 다음과 같습니다.
V: start.. V: I'm working. V: I'm working. V: I'm working. V: I'm working. V: I'm working. V: I'm working. V: I'm working. V: I'm working. V: I'm working. V: I'm working. V: stop
코루틴은 정해진 시간이 되면 코루틴의 완료되지 못할지라도 중지하게 할 수 있는데, 아래의 코드가 바로 그 예입니다.
print("start") val job = GlobalScope.launch { withTimeout(4000L) { repeat(10) { delay(1000L) print("I'm working.") } } } print("end")
결과는 다음과 같습니다.
V: start V: end V: I'm working. V: I'm working. V: I'm working.
코루틴은 채널(Channel)이라는 개념을 통해 코루틴에서 생성한 데이터를 또 다른 코루틴에게 전달할 수 있습니다. 아래의 코드는 코루틴에서 1~5까지의 정수에 대한 제곱값을 생성하면 생성된 정수 4개를 또 다른 코루틴에서 받아 출력하는 예입니다.
runBlocking { print("start") val channel = Channel<Int>() launch { for (x in 1..5) { channel.send(x * x) } } repeat(5) { val v = channel.receive() print("$v") } print("end") }
결과는 다음과 같습니다.
V: start V: 1 V: 4 V: 9 V: 16 V: 25 V: end
데이터를 생성하는 쪽이나 받는 쪽에서는 얼마나 많은 데이터를 생성할지 또는 받을지를 예측할 수 없는 경우가 대부분입니다. 데이터를 생성하는 쪽에서 채널의 close 함수를 호출하면 받는쪽에서 더 이상 데이터가 없다는 것을 인지하게 되는데, 아래는 이에 대한 코드 예입니다.
runBlocking { print("start") val channel = Channel<Int>() launch { for(x in 1..5) channel.send(x*x) channel.close() } for(y in channel) print("$y") print("end") }
결과는 다음과 같습니다.
V: start V: 1 V: 4 V: 9 V: 16 V: 25 V: end
다음은 데이터를 생성하는 코루틴을 함수화하여 이 함수를 통해 생성된 데이터를 처리하는 예제입니다.
runBlocking { print("start") val squares = procedureSquares() squares.consumeEach { print("$it") } print("end") } private fun CoroutineScope.procedureSquares(): ReceiveChannel<Int> = produce { for(x in 1..5) send(x*x) }
결과는 다음과 같습니다.
V: start V: 1 V: 4 V: 9 V: 16 V: 25 V: end
다음은 데이터를 생성하는 코루틴을 파이프라인 형태로 묶어 처리하는 것으로, 첫번째 코루틴에서 생성한 값을 또 다른 코루틴에서 받아 처리하여 또 다른 코루틴으로 전달하는 예제입니다.
runBlocking { print("start") val numbers = productNumbers() val squares = squares(numbers) for(i in 1..5) print("${squares.receive()}") print("end") coroutineContext.cancelChildren() } private fun CoroutineScope.productNumbers() = produce<Int> { var x = 1 while(true) { print("send ${x} on productNumbers") send(x++) delay(100) } } private fun CoroutineScope.squares(numbers:ReceiveChannel<Int>): ReceiveChannel<Int> = produce { for(x in numbers) { print("send ${x} on squares") send(x*x) } }
결과는 다음과 같습니다.
V: start V: send 1 on productNumbers V: send 1 on squares V: 1 V: send 2 on productNumbers V: send 2 on squares V: 4 V: send 3 on productNumbers V: send 3 on squares V: 9 V: send 4 on productNumbers V: send 4 on squares V: 16 V: send 5 on productNumbers V: send 5 on squares V: 25 V: end
데이터를 생성하는 코루틴은 1개지만, 이를 원활하게 처리하기 위해 여러개의 코루틴으로 생성된 데이터를 처리할 수 있습니다. 아래는 데이터를 생성하는 코루틴 1개와 생성된 데이터를 처리하는 5개의 코루틴에 대한 예제입니다.
runBlocking { val producer = productNumbers() repeat(5) { launchProcessor(it, producer) } delay(1000L) producer.cancel() } fun CoroutineScope.launchProcessor(id:Int, channel: ReceiveChannel<Int>) { launch { for(msg in channel) { print("Processor #$id received $msg") } } } private fun CoroutineScope.productNumbers() = produce<Int> { var x = 1 while(true) { print("send ${x} on productNumbers") send(x++) delay(100) } }
결과는 다음과 같습니다.
V: send 1 on productNumbers V: Processor #0 received 1 V: send 2 on productNumbers V: Processor #0 received 2 V: send 3 on productNumbers V: Processor #1 received 3 V: send 4 on productNumbers V: Processor #2 received 4 V: send 5 on productNumbers V: Processor #3 received 5 V: send 6 on productNumbers V: Processor #4 received 6 V: send 7 on productNumbers V: Processor #0 received 7 V: send 8 on productNumbers V: Processor #1 received 8 V: send 9 on productNumbers V: Processor #2 received 9 V: send 10 on productNumbers V: Processor #3 received 10
반대로 데이터를 생성하는 코루틴은 여러개이고 처리하는 코루틴은 1개인 경우도 있습니다. 아래는 데이터를 생성하는 코루틴 2개와 생성된 데이터를 처리하는 코루틴 1개에 대한 예제입니다.
runBlocking { val channel = Channel<String>() launch { sendString(channel, "foo", 200L) } launch { sendString(channel, "BAR", 500L) } repeat(6) { print("${channel.receive()}") } coroutineContext.cancelChildren() } private suspend fun sendString(channel: SendChannel<String>, s:String, time:Long) { while(true) { delay(time) channel.send(s) } }
결과는 다음과 같습니다.
V: foo V: foo V: BAR V: foo V: foo V: BAR
아래의 예제는 2개의 코루틴에서 하나의 데이터에 대해 어떤 처리를 해서 주고 받는 기능에 대한 코드입니다.
print("start") data class Ball(var hits:Int) suspend fun player(name:String, table: Channel) { for(ball in table) { ball.hits++ print("$name $ball") delay(300) table.send(ball) } } runBlocking { var table = Channel<Ball>() launch { player("ping", table) } launch { player("pong", table) } table.send(Ball(0)) delay(1000) coroutineContext.cancelChildren() } print("end")
결과는 다음과 같습니다.
V: start V: ping Ball(hits=1) V: pong Ball(hits=2) V: ping Ball(hits=3) V: pong Ball(hits=4) V: end
엄청 엄청 도움 됐습니다 감사합니다~!
좋은 정보 올려주셔서 감사합니다!
감사합니다…