본문 바로가기
Kotlin/Kotlin 프로그래밍

[Kotlin/Coroutine] 코루틴(Coroutine) - 동시성 프로그래밍

by 주 녕 2021. 6. 3.
반응형

모든 내용은 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다. 

 

동시성 프로그래밍

👆 동기와 비동기

  • 동기적(synchronous) : 프로그래밍에서 순서대로 작업을 수행하여 1개의 루틴을 완료하고 다른 루틴을 실행하는 방식
  • 비동기적(asynchronous) : 여러 개의 루틴이 선행 작업의 순서나 완료 여부와 상관없이 실행되는 방식

비동기 프로그래밍은 RxJava, Reactive와 같은 서드파티(third-party) 라이브러리에서 제공함

서드파티란, 기본으로 제공되는 표준 라이브러리가 아닌 다른 개발자(제3자)가 만든 라이브러리! 플러그인, 프레임워크, 유틸리티 API 등을 제공함!

 

👍 코틀린에서는 코루틴(Coroutine)을 서드파티가 아닌 기본으로 제공함

개별적인 작업을 루틴(routine)이라고 부르고, 여러 개의 루틴들이 협력(co)한다는 의미로 만들어진 합성어 → 코루틴(Coroutine)! 

 

 

블로킹과 넌블로킹

블로킹

코드의 여러 구간에서 요청된 작업이 마무리 될 때까지 멈춰 있는 현상

 

Doit 코틀린 프로그래밍의 500pg, <블로킹 형태의 태스크 수행>

  • 2개의 태스트(Task)가 있는 일반적인 형태의 프로그램 흐름
  • 첫번째 블로킹 : 태스트 A에서 입출력 과정이 수행될 때 내부 메모리 영역에서 해당 작업이 마무리 될 때까지 코드 수행을 멈춤
    • 태스크 A가 블로킹하는 동안 운영체제의 스케줄링 정책에 따라 우선순위가 낮은 또 다른 태스크가 실행될 수 있음
  • 두번째 블로킹 : 우선순위가 높은 태스크 A의 실행이 재개되면 우선순위가 낮은 태스크 B는 코드 수행을 멈춤
    • 태스크 A가 종료되면 다시 태스크 B가 재개됨

 

넌블로킹

Doit 코틀린 프로그래밍의 500pg, <넌블로킹 형태의 태스크 수행>

  • 프로세스에서 입출력 요청을 하더라도 운영체제에 의해 EAGAIN과 같은 시그널을 태스크 A가 받아 실행을 재개할 수 있음
    • 태스크 A는 다른 루틴을 수행하다가 내부적으로 입출력 완료 시그널을 받고 나서 콜백 루틴(Callback Routine) 등을 호출하여 이후의 일을 처리할 수 있는 것
    • 코드의 흐름을 멈추지 않고 다른 루틴을 먼저 수행할 수 있기 때문에 실행시간이 더 빠르고 더 나은 성능을 보여줌
  • 병행 수행(Concurrency) : 여러 개의 코어가 태스크를 동시에 수행하는 것
    • 태스크 A와 B가 동시에 수행되는 것처럼 보이지만 프로세서 코어 수에 따라 동시에 수행될 수도 있고, 2개의 태스크를 자주 교환하여 동시에 수행되는 것처럼 보이게 할 수 있음

 

 

프로세스와 스레드

👆 태스크 큰 실행 단위인 프로세스 / 작은 실행 단위인 스레드를 말함

  • 프로세스 : 운영체제로부터 자원을 할당 받은 작업의 단위
    • 하나의 프로그램이 실행되면 프로세스가 실행됨
    • 프로세스와 프로세스는 서로 완전히 독립되어 있음
    • 프로세스는 문맥(Context)이라고 하는 코드, 데이터, 열린 파일 식별자, 동적 할당 영역, 스택 등을 모두 포함
    • 따라서 프로세스 간 문맥 교환(Context-Switching) 할 때 많은 비용이 듦
  • 스레드 : 프로세스가 할당 받은 자원을 이용하는 실행 흐름의 단위
    • 스레드는 레지스터와 자신의 스택만 독립적으로 가짐
    • 대부분의 문맥은 프로세스 안에서 스레드끼리 공유함 → 문맥 교환 비용이 낮아 프로그래밍에서 많이 사용됨
    • 여러 개의 스레드 구성 시 코드가 복잡해짐

 

👍 틀린의 코루틴 개념을 사용하면 전통적인 스레드 개념을 만들지 않고도 좀 더 쉽게 비동기 프로그래밍을 할 수 있음!

→ 문맥 교환이 없고 최적화된 비동기 함수를 통해 비선점형으로 작동하는 특징이 있어 협력형 멀티태스킹(Cooperative Multitasking)을 구현할 수 있게 해줌

*협력형 멀티태스킹이란 태스크들이 자발적으로 양보하며 실행을 바꿀 수 있는 개념임.

프로그램에서 태스크를 수행할 때 운영체제를 사용할 수 있게 하고 특정한 작업에 작업 시간을 할당하는 것을 '선점한다'라고 하는데,

선점형 멀티태스킹(Preemptive Multitasking)은 운영체제가 강제로 태스크의 실행을 바꾸는 것.

 

스레드 생성하기

기존의 자바에서 사용하던 스레드를 생성하는 방법 >

// Thread 클래스를 상속받아 구현
class SimpleThread: Thread() {
    override fun run() {
        super.run()
        println("Current Threads: ${Thread.currentThread()}")
    }
}

// Runnable 인터페이스로부터 run() 구현
class SimpleRunnable: Runnable {
    override fun run() {
        println("Current Threads: ${Thread.currentThread()}")
    }
}

fun main() {
    val thread = SimpleThread()
    thread.start()

    val runnable = SimpleRunnable()
    val thread1 = Thread(runnable)
    thread1.start()
}
  • Thread 클래스를 상속받은 경우 → 다중 상속이 허용되지 않기 때문에 Thread 이외의 클래스를 상속할 수 없음
  • Runnable 인터페이스를 구현한 경우 → 다른 클래스 상속 가능
  • 스레드 구현
    • 스레드에서 실행할 코드는 run() 메서드를 오버라이딩 해서 구현
    • 이것을 실행하려면 해당 클래스 객체의 start() 메서드를 호출 → 각 스레드의 run() 본문을 수행하는 독립된 실행 루틴 동작
    object : Thread() {
        override fun run() {
            super.run()
            println("Current Threads(object): ${Thread.currentThread()}")
        }
    }.start()

    Thread {
        println("Current Threads(lambda): ${Thread.currentThread()}")
    }.start()
  • 익명 객체를 이용하여 클래스 객체를 만들지 않고 실행한 경우
    • 객체 표현식에 의해 익명 클래스로 생성하고 run()을 오버라이딩해서 구현함
  • Runnable을 전달하는 람다식

 

사용자 함수를 통한 스레드를 생성하는 방법 >

public fun thread(start: Boolean = true, isDaemon: Boolean = false,
                    contextClassLoader: ClassLoader? = null, name: String? = null,
                    priority: Int = -1, block: () -> Unit): Thread {
    val thread = object : Thread() {
        override fun run() {
            super.run()
            block()
        }
    }

    if (isDaemon)  // 백그라운드 실행 여부
        thread.isDaemon = true
    if (priority > 0)  // 우선순위(1:낮음 ~ 5:보통 ~ 10:높음)
        thread.priority = priority
    if (name != null)  // 이름
        thread.name = name
    if (contextClassLoader != null)
        thread.contextClassLoader = contextClassLoader
    if (start)
        thread.start()
    return thread
}

fun main() {
    thread(start = true) {
        println("Current Threads(Custom function): ${Thread.currentThread()}")
        // Current Threads(Custom function): Thread[Thread-0,5,main]
        println("Priority: ${Thread.currentThread().priority}")  // Priority: 5
        println("Name: ${Thread.currentThread().name}")  // Name: Thread-0
        println("Name: ${Thread.currentThread().isDaemon}")  // Name: false
    }
}
  • 스레드의 우선순위, 백그라운드 여부, 이름 등 스레드가 가져야할 각종 옵션 변수를 손쉽게 설정할 수 있음
    • 옵션을 비우변 기본값이 사용됨
    • 보일러플레이트한 코드를 숨길 수 있어 thread() 함수만으로 깔끔한 코딩이 가능함
  • 우선순위(Priority) : 운영체제에 따라 다르지만 JVM에서는 10단계로 값이 높으면 높은 우선순위를 가짐
  • 데몬 여부 : 백그라운드 서비스를 제공하기 위한 스레드 생성 (낮은 우선순위가 부여됨)

*보일러플레이트(Boilerplate) : 코드가 반복되어 자주 쓰이지만 매번 작성하기 번거롭고 읽기 어려운 많은 양의 코드

→ 보일러플레이트한 코드를 제거해서 자주 사용되는 루틴을 간략화하는 것이 코틀린의 목표이기도 함

 

 

스레드 풀 사용하기

어플리케이션 비즈니스 로직을 설계할 때는 스레드가 자주 재사용됨

→ 몇 개의 스레드를 먼저 만들어 놓고 필요에 따라 재사용하도록 설계할 수 있음

newFixedThreadPool()로 스레드를 인자의 수만큼 만들고 작업을 수행할 때 여기에서 재사용 가능한 스레드를 고르게 함

 

 

 

반응형

댓글