강의

멘토링

커뮤니티

Cộng đồng Hỏi & Đáp của Inflearn

Hình ảnh hồ sơ của 96chlwogur2
96chlwogur2

câu hỏi đã được viết

Coroutine trong 2 giờ

Bài học 3. Công việc và trình tạo Coroutine

한 suspend fun 의 반환값이 다른 suspend fun의 파라미터로 쓰일 때

Đã giải quyết

Viết

·

311

1

fun main(): Unit = runBlocking {
   val job1 = async { apiCall1() }
   val job2 = async { apiCall2(job1.await()) }
   printWithThread(job2.await())
}

suspend fun apiCall1(): Int {
   delay(1_000L)
   return 1
}

suspend fun apiCall2(num: Int): Int {
   delay(1_000L)
   return num + 2
}

위와 같은 코드에서, 코루틴을 사용해도 apiCall1() 의 반환값이 apiCall2()의 인자로 사용되기 때문에, apiCall1()이 완료된 후에 apiCall2()가 실행되는 것은 이해했습니다. 그리고 코루틴을 사용할 때의 이점이 마치 한줄한줄 동기식 코드를 작성할 때처럼 비동기 코드를 사용할 수 있다. 로 이해했습니다.

그럼 위와 같은 상황에서는 굳이 코루틴을 사용하지 않고, 아래와 같이 동기적으로 코드를 작성해도 같은 것일까요? (두 apiCall1,2를 사용하는 외부 메서드가 없을 때)

fun main() {
   val job1 = apiCall1()
   val job2 = apiCall2(job1)
   printWithThread(job2)
}

fun apiCall1(): Int {
   delay(1_000L)
   return 1
}

fun apiCall2(num: Int): Int {
   delay(1_000L)
   return num + 2
}

비동기 처리가 처음이다 보니 직관적으로 잘 와닿지 않는 부분이 많아 자꾸 질문드리네요 ㅜㅜ 죄송합니다.

kotlincoroutine

Câu trả lời 2

1

lannstark님의 프로필 이미지
lannstark
Người chia sẻ kiến thức

안녕하세요 응애님! 🙂 좋은 질문 감사합니다~~

비동기 자체가 직관적이지 않다보니 이해하기 되게 어려운 것 같아요!! 편하게 계속 질문 남겨주셔도 괜찮습니다~! 😊

하나씩 답변 드려 볼게요!!

 

[1. 본문 - awiat()을 사용하면 굳이 비동기 코드를 사용하지 않아도 되는가]

  • 결론부터 말씀드리면 아래 코드 처럼 두 번째 작업이 첫 번째 작업에 의존하는 상황이라면, 비동기 코드를 사용하건~ 동기 코드를 사용하건 최종 시간은 동일합니다.

val job1 = async { apiCall1() }
val job2 = async { apiCall2(job1.await() }
  • 예를 들어 apiCall1()이 1초 apiCall2()가 1초 걸린다면, 어차피 apiCall2()apiCall1() 이 완전히 끝난 후 호출할 수 있기 때문에 총 2초가 걸리게 되고, 이는 그냥 동기적으로 작성하는 것과 같죠

 

하지만, 단순히 blocking + sync 스타일로 작성하는게 아니라 코루틴을 활용해 non-blocking + async 스타일로 작성하게 되면, 내부적으로는 조금 다르게 동작합니다.

blocking + sync 스타일로 작성하면 스레드가 2초간 완전히 blocking 되어 그 스레드를 다른 곳에 활용할 수 없지만, non-blocking + async 스타일로 작성하게 되면, API를 호출해야 할 때만 잠시 스레드를 사용하고, 그 외에는 다른 작업에 해당 스레드를 사용할 수 있게 되죠

결론적으로, 최종 시간은 동일하되 스레드 사용량은 코드 구현에 따라 다를 수 있습니다.

 

[2. 리스트를 순회하면서 suspend fun A, B를 호출했을 때는 단건 호출과 다르게 비동기 코드의 이점이 있다? (리스트 하나의 원소의 처리가 모두 다 끝나기를 기다리지 않고 다음 원소 처리로 넘어가는가?)]

이는 구현에 따라 다릅니다!

  1. 호출하는 함수가 Thread를 blocking 하고, for 문 전체를 하나의 Thread에서 돌리는 경우

    1. 이 경우는 loop의 로직 전체가 실행되어야만 다음 loop로 넘어갈 수 있기 때문에 리스트 처리 역시 하나씩 하나씩 이루어지게 됩니다.

  2. 호출하는 함수가 Thread를 blocking 하고, for 문 전체를 여러개의 Thread에서 돌리는 경우

     

    1. 이 경우는 몇개의 Thread에서 돌리느냐에 따라 여러 루프 로직이 동시에 실행되게 됩니다.

  3. 호출하는 함수가 Thread를 blocking 하지 않고, for 문 전체를 하나의 Thread에서 돌리는 경우

    1. 이 경우는 리스트 하나의 원소 처리가 모두 끝나지 않더라도 다음 리스트를 처리할 수 있습니다.

  4. 호출하는 함수가 Thread를 blocking 하지 않고, for 문 전체를 여러개의 Thread에서 돌리는 경우

    1. 3번과 동일하게, 리스트의 여러 원소를 동시에 처리할 수 있습니다.

 

Thread를 blocking 한다에 대해 더 말씀드리면

  • Thread.sleep(1_000L) 과 같은 코드로 스레드를 잠시 재워둘 수도 있고

  • delay(1_000L) 과 같은 코드로 해당 코루틴을 잠시 멈춰둘 수 있죠!

전자는 Thread자체가 blocking 되고, 후자는 Thread를 blocking 하지는 않습니다.

 

[3. EDIT - 리스트의 각 원소마다 새로운 코루틴이 생성되어 각각의 원소를 병렬처리할 수 있을 것으로 보았는데 맞을까요?]

이 역시 내부 구현이 중요합니다! 현재 구현상 리스트는 하나의 스레드에서만 돌도록 되어 있고요!

만약 리스트에서 특정 함수를 호출했는데, 그 함수 내부에 Thread를 blocking 하는 코드가 들어 있다면, 병렬 처리가 불가능합니다.

Thread를 blocking 하는 코드가 없다면, 위에서 말씀드린 3번 경우와 비슷하게 새로운 코루틴이 계속해서 생기며 각각의 원소를 병렬 처리할 수 있을거에요! 😊

 

답변이 도움이 되었으면 좋겠습니다.

감사합니다! 🙏

96chlwogur2님의 프로필 이미지
96chlwogur2
Người đặt câu hỏi

질문량이 상당했는데 하나하나 정성스럽게 답변해주셔서 정말 감사합니다.

코루틴을 사용하여 비동기 코드를 작성하면 내부적으로 스레드를 다른 곳에 사용할 수 있게 되겠네요. 실행 속도에만 집중하다보니 그러한 이점을 생각지 못했습니다. (_ _)

3. 사실 제가 지금 작성하고 있는 코드에서 코루틴을 적용하려고 하다 보니 좀 지엽적인 질문이 되었는데요, 상세하게 답변 달아주셔서 정말 감사합니다! 저는 현재 유저 리스트를 순회하면서 각각의 유저에 대해 외부 API 2개를 호출하는 동작을 개발하고 있었습니다. 외부 API 호출 또한 네트워크 I/O 작업이므로 스레드를 Blocking 하게 되니까, EDIT)의 코드와 비슷한 형태로 코루틴을 적용하더라도 말씀 주신 4가지 경우 중

  1. 호출하는 함수가 Thread를 blocking 하고, for 문 전체를 하나의 Thread에서 돌리는 경우

    1. 이 경우는 loop의 로직 전체가 실행되어야만 다음 loop로 넘어갈 수 있기 때문에 리스트 처리 역시 하나씩 하나씩 이루어지게 됩니다.

위와 같은 상황에 해당하기 때문에 병렬 처리가 되지 않을 것으로 이해하였습니다. ㅎㅎ

lannstark님의 프로필 이미지
lannstark
Người chia sẻ kiến thức

네네 맞습니다! 맞게 이해하셨습니다! 🙂

혹시나 스프링이라는 프레임워크를 사용하고 계시는거라면, 외부 API를 호출할 때 WebClient를 이용해 non-blocking 호출을 할 수도 있습니다. 그렇게 되면 하나의 thread에서 여러 API 호출을 돌리려 할 때 훨씬 성능이 개선될 거에요!

좋은 방법 잘 찾으셨으면 좋겠습니다. 감사합니다! 🙏

0

96chlwogur2님의 프로필 이미지
96chlwogur2
Người đặt câu hỏi

앗 그리고 추가적으로, 만약 아래와 같이 main 함수 내에서 List를 돌면서 apiCall1, apiCall2를 호출했을 때,

  1. 동기적 코드

fun main() {
   val list: List<Example> = listOf(Example(1), Example(2)... ,Example(100))

   list.forEach { ex ->
      val job1 = apiCall1(ex)
      val job2 = apiCall2(job1)
   } 
}

fun apiCall1(ex: Example): Int {
   // 네트워크를 타는 어떤 외부 api A
   return 1
}

fun apiCall2(num: Int): Int {
   // 네트워크를 타는 어떤 외부 api B
   return num + 2
}
  1. 비동기적 코드

fun main(): Unit = runBlocking {
   val list: List<Example> = listOf(Example(1), Example(2)... ,Example(100))

   list.forEach { ex ->
      val job1 = async { apiCall1(ex) }
      val job2 = async { apiCall2(job1.await()) }
   } 
}

suspend fun apiCall1(ex: Example): Int {
   // 네트워크를 타는 어떤 외부 api A
   return 1
}

suspend fun apiCall2(num: Int): Int {
   // 네트워크를 타는 어떤 외부 api B
   return num + 2
}

이 상황에서는 apiCall2() 의 실행은 마찬가지로 apiCall1() 이 완료된 후에 진행되지만,

비동기적으로 코드를 작성했을 경우에는 리스트의 첫번째 원소의 apiCall1()이 끝나면, apiCall2() 가 완료되기를 기다리지 않고, 바로 다음 원소의 apiCall1() 이 실행될 것으로 예상했는데... 혹시 맞을까요?

forEach 자체가 비동기 함수가 아니라서 아닐 수도 있겠네요...ㅜㅜ

 

첫번째 질문과 두번째 질문을 정리하자면,

1. 한 suspend fun A()의 반환값을 파라미터로 받는 다른 suspend fun B()가 있으면, B의 실행은 A가 종료된 이후에 실행되므로 동기적 코드에 비교했을 때 큰 이점이 없다?

2. 리스트를 순회하면서 suspend fun A, B를 호출했을 때는 단건 호출과 다르게 비동기 코드의 이점이 있다? (리스트 하나의 원소의 처리가 모두 다 끝나기를 기다리지 않고 다음 원소 처리로 넘어가는가?)

입니다.


EDIT) 혹시 다음과 같이 작성하면 리스트의 각 원소마다 새로운 코루틴이 생성되어 각각의 원소를 병렬처리할 수 있을 것으로 보았는데 맞을까요? 그리고 아래와 같은 상황이라면 apiCall1, apiCall2는 suspend fun이 아니어도 될 것 같습니다..!

fun main(): Unit = runBlocking {
   val list: List<Example> = listOf(Example(1), Example(2)... ,Example(100))

   list.forEach { ex ->
      launch {
         val job1 = apiCall1(ex)
         val job2 = apiCall2(job1)
      }      
   } 
}


fun apiCall1(ex: Example): Int {
   // 네트워크를 타는 어떤 외부 api A
   return 1
}

fun apiCall2(num: Int): Int {
   // 네트워크를 타는 어떤 외부 api B
   return num + 2
}
Hình ảnh hồ sơ của 96chlwogur2
96chlwogur2

câu hỏi đã được viết

Đặt câu hỏi