본문 바로가기

카테고리 없음

코틀린 코루틴의 정석 (1)

728x90

JVM 프로세스, 스레드

메인 스레드: 프로세스의 시작, 끝을 함께하는 매우 중요한 역할을한다.

예외로 인해 메인 스레드가 강제로 종료되면, 프로세스도 강제 종료된다.

 

단일 스레드 프로그래밍

스레드 하나만 사용해서 실행되는 애플리케이션

단일 스레드의 한계 : 스레드는 하나의 작업을 수행하는 동안 다른 작업을 수행하지 못한다.

응답성이 안 좋아진다.

 

멀티스레드 프로그래밍, 스레드를 여러개 사용해 작업을 처리하는 프로그래밍 기법

각각의 스레드가 한번에 하나의 작업을 처리하므로 여러작엉블 동시에 처리하는 것이 가능해진다.

(병렬처리)

 

1.3 스레드, 스레드 풀을 이용한 멀티 스레드 프로그래밍

 

Thread 클래스를 이용한 방법 및 한계

Thread 클래스를 상속하는 클래스

 class ExampleThread : Thread() {
        override fun run() {
            println("${Thread.currentThread()}  새로운 스레드 시작")
            Thread.sleep(2000L)
            println("${Thread.currentThread()}  새로운 스레드 끝")
        }
    }
    @Test
    fun test1() {
        println("${Thread.currentThread()}  메인 스레드 시작")
        ExampleThread().start()
        Thread.sleep(1000)
        println("${Thread.currentThread()}  메인 스레드 끝")
    }

 

사용자 스레드, 데몬 스레드

사용자 스레드는 우선도가 높고, 데몬 스레드는 우선도가 낮은 스레드

JVM 프로세스가 종료되는 시점은 우선도가 높은 사용자 스레드가 모두 종료되는 시점

단일 스레드만 사용했을떄는 메인 스레드만 사용자 스레드이기 떄문에 메인 스레드가 종료될때 프로세스가 종료된다.

멀티 스레드는 스레드 중 사용자 스레드가 모두 종료되는 시점에 프로세스가 종료된다.

Thread 클래스를 상속한 클래스는 사용자 스레드로 생성된다. (isDaemon 으로 변경가능)

 

한계 : 매번 새로운 스레드가 생성된다. (스레드 생성 비용이 비싸기 떄므로 성능면에서 좋지 않다.)

스레드 생성과 관리를 개발자가 한다. (프로그램 복잡도가 올라가고, 실수로 인해 오류, 메모리 누수가 생길수 있다.)

 

 

Executors 프레임워크 사용하기

스레드를 생성하고 관리하는 스레드 풀을 사용한다.

미리 스레드 풀을 생성하고, 작업을 요청 받으면 쉬고 있는 스레드에 작업을 분배한다. 스레드가 작업을 종료하더라도 스레드를 종료하지 않고, 재사용한다.

개발자는 스레드풀에 속한 스레드의 개수와 설정을 설정하고, 스레드 풀에 작업만 제출하는 역할

 

fun main() {
  val startTime = System.currentTimeMillis()
  // ExecutorService 생성
  val executorService: ExecutorService = Executors.newFixedThreadPool(2)

  // 작업1 제출
  executorService.submit {
    println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}]작업1 시작")
    Thread.sleep(1000L) // 1초간 대기
    println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}]작업1 완료")
  }
  // 작업2 제출
  executorService.submit {
    println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}]작업2 시작")
    Thread.sleep(1000L) // 1초간 대기
    println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}]작업2 완료")
  }

  // ExecutorService 종료
  executorService.shutdown()
}
/*
// 결과:
[pool-1-thread-1][지난 시간: 4ms]작업1 시작
[pool-1-thread-2][지난 시간: 4ms]작업2 시작
[pool-1-thread-1][지난 시간: 1009ms]작업1 완료
[pool-1-thread-2][지난 시간: 1009ms]작업2 완료

Process finished with exit code 0
*/

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

 

3개의 작업을 제출하면, 작업 1, 작업2는 동시에 실행되지만, 작업3은 작업1이 완료된 후에 실행된다. (스레드 풀 크기가 2이므로)

 

ExecutorService 내부 구조

 

- 작업을 적재하는 대기큐 (BlockingQueue)

- 작업을 수행하는 스레드풀

 

개발자는 ExecutorService 내부 구현의 동작을 신경 쓰지 않아도 된다. (개발자 복잡도 감소, 실수로 인한 오류 줄어든다.)

ExecutorService의 한계

 

스레드 블로킹 : 스레드가 아무것도 하지 못하고 사용될수 없는 상태 (스레드는 비싼 자원이므로 애플리케이션 성능이 떨어지게 된다.)

여러 스레드가 동기화 블록에 동시에 접근하는 경우 하나의 스레드만 동기화 블록에 접근이 허용되기 때문에 발생할수 있다.

fun main() {
  val executorService: ExecutorService = Executors.newFixedThreadPool(2)
  val future: Future<String> = executorService.submit<String> {
    Thread.sleep(2000)
    return@submit "작업 1완료"
  }

  val result = future.get() // 메인 스레드가 블로킹 됨
  println(result)
  executorService.shutdown()
}

 

Java1.8에서는 Future 객체의단점을 보완한 CompletableFuture객체 가 나왔다. 

리액티브 프로그래밍을 지원하느 RxJava 등장

 

기존 멀티 스레드 프로그래밍의 한계와 코루틴

 

 Thread 1의 결과가 필요한 Thread0 

Thread0이 마저 작업을 수행하려면 Thread1의 결과물이 필요허ㅏ므로, 대기해야한다. (스레드 블로킹)

 

스레드

fun main() {
  val startTime = System.currentTimeMillis()
  val executor = Executors.newFixedThreadPool(2)

  // CompletableFuture 생성 및 비동기 작업 실행
  val completableFuture = CompletableFuture.supplyAsync({
    Thread.sleep(1000L) // 1초간 대기
    return@supplyAsync "결과" // 결과 반환
  }, executor)

  // 비동기 작업 완료 후 결과 처리를 위한 체이닝 함수 등록
  completableFuture.thenAccept { result ->
    println("[${getElapsedTime(startTime)}] $result 처리") // 결과 처리 출력
  }

  // 비동기 작업 실행 도중 다른 작업 실행
  println("[${getElapsedTime(startTime)}] 다른 작업 실행")

  executor.shutdown()
}
/*
// 결과:
[지난 시간: 11ms] 다른 작업 실행
[지난 시간: 1008ms] 결과 처리
*/

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

 

CompletableFuture 객체에 결과가 반환됐을때 실행할 콜백을 thenAccept 함수를 통해 등록할수 있다.

Future 객체는 get()으로 스레드를 블로킹하지만, CompletableFtuure는 콜백을 등록하므로 메인 스레드가 블로킹되지 않아 다른 작업을 실행할수 있따.

 

 

 

코루틴

 

작업 단위 코루틴을 통해 스레드 블로킹 문제를 해결한다.  (일시 중단할수 있는 작업단위)

일시 중단되면 더 이상 스레드 사용이 필요하지 않으므로 스레드 사용권한을 양보하며 양보된 스레드는 다른 작업을 실행하는데 사용할수 있다. 일시 중단된 코루틴은 재개 시점에 다시 스레드에 할당돼 실행된다.

 

프로그래머가 코루틴을 만들어 코루틴 스케줄러에 넘기면 코루틴 스케줄러는 자신이 사요ㅕㅇ할수 있는 스레드나 스레드풀에 해당 코루틴을 분배해 작업을 수행할수 있다. (스레드 사용 최적화, 스레드가 블로킹 되는 상황 방지)

코루틴은 스레드에 비해 생성, 전환 비용이 적게 들고, 스레드에 자유롭게 뗏다 붙였다 할수 있어 리소스와 시간이 줄어든다.(경량 스레드)

 

구조화된 동시성을 통해 비동기 작업을 안전하게 만든다.

예외 처리를 효과적으로 처리할수 있도록 한다.