코루틴은 스레드와 기능적으로 같지만, 스레드에 비교하면 좀더 가볍고 유연하며 한단계 더 진화된 병렬 프로그래밍을 위한 기술입니다. 하나의 스레드 내에서 여러개의 코루틴이 실행되는 개념인데, 아래의 코드는 동일한 기능을 스레드와 코루틴으로 각각 구현한 코드의 예시입니다.
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