인프런 커뮤니티 질문&답변

보키님의 프로필 이미지
보키

작성한 질문수

코틀린 코루틴 완전 정복

코루틴의 스레드 양보

스레드 양보 예제 + 코루틴/멀티스레드 사용 예시 질문

해결된 질문

작성

·

31

1

- 학습 관련 질문을 남겨주세요. 질문을 상세히 작성하면 더 좋습니다.
- 서로 예의를 지키며 존중하는 문화를 만들어가요.
- 잠깐! 인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요.

section10의 code4/Code10-4에서 보면

코드가 아래와 같이 되는데

package section10.code4

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  repeat(10) { repeatTime ->
    launch {
      Thread.sleep(1000L) // 1초 동안 스레드 블로킹(코루틴의 스레드 점유 유지)
      println("[${Thread.currentThread().name}] 작업 실행 ")
      println("[${getElapsedTime(startTime)}] 코루틴${repeatTime} 실행 완료")
    }
  }
}

fun getElapsedTime(startTime: Long): String = "지난 시간: ${System.currentTimeMillis() - startTime}ms"

보통 이런 코드는 이렇게 멀티스레드로 처리하지 않나요..?

import java.util.concurrent.Callable
import java.util.concurrent.Executors

fun main() {
    val startTime = System.currentTimeMillis()
    val es = Executors.newFixedThreadPool(10)
    val callTasks = mutableListOf<Callable<Int>>()
    repeat(10) { repeatTime ->
        val callTask = Callable {
            println("[${Thread.currentThread().name}] 작업 실행 ")
            return@Callable repeatTime
        }
        callTasks.add(callTask)
    }
    val results = es.invokeAll(callTasks)

    // 결과 출력
    results.forEach { future ->
        println("Result: ${future.get()}")
    }
    println("[${getElapsedTime(startTime)}] 실행 완료")
    es.close()
}

fun getElapsedTime(startTime: Long): String = "지난 시간: ${System.currentTimeMillis() - startTime}ms"

음.. 그리고 스레드 1개를 만들어서 run을 시키면 1MB정도의 메모리 비용이 발생하고 context switch도 일어나지만, 코루틴은 훨씬 더 값싸다고 알고있습니다

추가로 아직 r2dbc처럼 비동기 트랜잭션 처리 등.. 이게 지원이 좀 미약하다고 알고있습니다. 서버의 작업은 대체로 CPU를 사용하는 부분이 그렇게 많이 없고 DB에 쓰고 값을 가져오는 동기화 코드, 순차처리 작업이 많은걸로 알고 있습니다.

그럼 언제 멀티스레드를 사용하는게 좋고, 언제 코루틴을 사용하는게 좋을까요?

답변 2

2

조세영님의 프로필 이미지
조세영
지식공유자

보키님 안녕하세요. 지식 공유자 조세영입니다.

 

section10의 code4/Code10-4에서 보면

코드가 아래와 같이 되는데

package section10.code4

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  repeat(10) { repeatTime ->
    launch {
      Thread.sleep(1000L) // 1초 동안 스레드 블로킹(코루틴의 스레드 점유 유지)
      println("[${Thread.currentThread().name}] 작업 실행 ")
      println("[${getElapsedTime(startTime)}] 코루틴${repeatTime} 실행 완료")
    }
  }
}

fun getElapsedTime(startTime: Long): String = "지난 시간: ${System.currentTimeMillis() - startTime}ms"

보통 이런 코드는 이렇게 멀티스레드로 처리하지 않나요..?

네 맞습니다. 이런 코드는 보통 멀티 스레드로 처리합니다. 이 예제에서는 코루틴이 스레드 양보를 하지 않고 스레드를 블로킹 시키면, 일반적인 스레드를 사용하는 것과 다름이 없음을 보여줍니다.

import java.util.concurrent.Callable
import java.util.concurrent.Executors

fun main() {
    val startTime = System.currentTimeMillis()
    val es = Executors.newFixedThreadPool(10)
    val callTasks = mutableListOf<Callable<Int>>()
    repeat(10) { repeatTime ->
        val callTask = Callable {
            println("[${Thread.currentThread().name}] 작업 실행 ")
            return@Callable repeatTime
        }
        callTasks.add(callTask)
    }
    val results = es.invokeAll(callTasks)

    // 결과 출력
    results.forEach { future ->
        println("Result: ${future.get()}")
    }
    println("[${getElapsedTime(startTime)}] 실행 완료")
    es.close()
}

fun getElapsedTime(startTime: Long): String = "지난 시간: ${System.currentTimeMillis() - startTime}ms"

음.. 그리고 스레드 1개를 만들어서 run을 시키면 1MB정도의 메모리 비용이 발생하고 context switch도 일어나지만, 코루틴은 훨씬 더 값싸다고 알고있습니다

맞습니다. 스레드가 만들어지고 스캐쥴링 된 후, OS 레벨의 스레드에 의해 실행된 경우 sleep 함수가 호출되면 시에는 해당 시간만큼 cpu를 사용하지 않게 되고, 그 동안 스캐쥴링된 다른 스레드가 실행됩니다. 이런 경우 혹은 실행되는 스레드가 바뀔 때마다 context switch가 일어나는데요. Context Switching 비용은 말씀 해주신 것 처럼 비쌉니다.

하지만, 코루틴은 힙 영역의 메모리에 저장되는 객체이고, 하나의 스레드를 서로 양보하면서 사용할 때 Continuation이라 불리는 실행 정보 객체를 사용해 실행 정보를 저장하고 복구하기 때문에 스레드에 비해 훨씬 메모리를 덜 차지하고 가볍습니다.

추가로 아직 r2dbc처럼 비동기 트랜잭션 처리 등.. 이게 지원이 좀 미약하다고 알고있습니다. 서버의 작업은 대체로 CPU를 사용하는 부분이 그렇게 많이 없고 DB에 쓰고 값을 가져오는 동기화 코드, 순차처리 작업이 많은걸로 알고 있습니다.

그럼 언제 멀티스레드를 사용하는게 좋고, 언제 코루틴을 사용하는게 좋을까요?

먼저 코루틴도 멀티 스레드를 사용합니다.

다만, 코루틴 이전의 멀티 스레드 프로그래밍 API들은 비동기를 구현하기 위해 콜백을 사용해 코드가 코드의 유지 보수 비용이 늘어나는 문제가 있었는데요. 코루틴은 기존의 동기 방식의 코드처럼 짜는데 내부적으로는 비동기적으로 동작하게 만들 수 있다는 것이 가장 큰 장점입니다.

따라서 만약 CPU를 계속해서 사용하는(연산이 계속해서 일어나는) 작업이 기존의 멀티 스레드 방식으로 만들어져 있다면 바꿀 필요가 없고, DB에 값을 쓰고 가져오는 코드 같이 I/O 작업이 주로 일어나는 코드는 코루틴을 사용하는 것이 좋습니다. 다만, 말씀 주신 것처럼 현재 사용하시는 프레임웍에 따라 코루틴에 대한 지원이 충분하지 않을 수 있기 때문에 이런 부분을 잘 고려해야 합니다.

또한 어떤 라이브러리들은 외부에 노출된 API를 보면 코루틴을 사용하는 것처럼 보이지만, 내부에서는 blocking call을 하는 경우도 있어서, 이런 경우에는 코루틴으로 전환하시는 이점이 없을 수 있기 때문에 잘 확인하시고 결정 하셔야 할 것 같습니다.

보키님의 프로필 이미지
보키
질문자

세영님.. 답변 감사합니다!!

아직 비동기만으로 이루어진 서버(WebFlux)등을 다뤄보지는 않고 멀티스레드쪽만 공부를 했어서 ExecutorService를 사용하면 또 다를텐데라는 생각에서 질문을 드렸었습니다..! 충분히 이해했습니다!

안드로이드라면 UI를 그리는 스레드와 내부적으로 자원을 처리하는 스레드, 서버와 통신하는 스레드.. 이렇게 구분이 될 수 있을텐데..ㅠ 주니어 서버 개발자라서 아직 어디에 어떻게 사용해야되는지 몰라서 어렵네요..

아침부터 답변도 달아주시고.. 다시 한번 감사합니다!

추가로 인용구를 답변처럼 써주시는 부분 인상깊었습니다! 저도 나중에 요렇게 해봐야겠네요ㅎㅎ

좋은하루되세요 지식공유자님

0

안녕하세요, 인프런 AI 인턴입니다. 보키님.

코루틴과 멀티스레드를 비교하여 사용하는 경우에 대한 질문을 주셨네요. 코루틴과 멀티스레드는 비동기 작업을 처리하는 두 가지 방식으로, 각각 장단점이 있습니다. 아래에서 좀 더 자세히 설명드리겠습니다.

코루틴과 멀티스레드의 차이

  1. 코루틴은 비동기 작업을 처리하는 경량화된 방법으로, 일반적으로 많은 수의 병렬 작업이 필요한 경우 적합합니다. 코루틴은 메모리 사용량이 적고, 스레드 전환에 드는 비용을 줄여줍니다. 특히, 비동기 I/O 작업에 강점이 있으며, UI와 같은 메인 스레드를 차단하지 않고 작업을 수행할 수 있습니다.

  2. 멀티스레드는 각 스레드가 시스템의 코어에서 병렬로 실행될 수 있게 하여 CPU를 많이 사용하는 작업에 유리합니다. 하지만, 스레드 생성시에 높은 비용이 들며, 컨텍스트 스위칭이나 자원 경합으로 인해 복잡성이 증가할 수 있습니다.

언제 코루틴을 사용하고 언제 멀티스레드를 사용할까?

  • 코루틴이 유리할 때:

    • 비동기 I/O 작업 및 네트워크 요청에서 불필요한 스레드 대기를 줄여 성능을 높이고자 하는 경우.
    • UI 응답성을 유지하면서 병렬 처리를 하고자 할 때.
    • 경량 작업을 대량으로 처리해야 할 때, 특히 I/O 바운드 작업.
  • 멀티스레드가 유리할 때:

    • CPU 바운드 작업으로, 각 작업이 충분히 많은 연산량을 요구하여 여러 코어에서 병렬로 실행될 필요가 있을 때.
    • 단일 스레드로는 처리하기 어려운 복잡한 멀티 스레드 동기화가 필요한 경우.

추가로 알아두면 좋은 점

  • 코루틴은 Kotlin의 언어 수준 지원 덕분에 코드가 보다 명확하게 작성되고 디버깅이 상대적으로 수월하다는 장점이 있습니다.
  • 반면, 멀티스레드는 자원을 많이 소모하고 복잡한 구조를 가지지만, 잘 설계된 멀티스레드 어플리케이션은 매우 높은 성능을 발휘할 수 있습니다.

아래 링크를 통해 관련된 내용을 더 깊게 이해하실 수 있을 것입니다:
- 코루틴과 멀티스레드 비교 (강의 링크)

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

보키님의 프로필 이미지
보키

작성한 질문수

질문하기