아래 교재 및 문서를 보고 공부한 내용을 정리한 공간입니다.

Hello, Concurrent World!

프로세스
  • 실행중인 어플리케이션의 인스턴스. 즉, 어플리케이션 ∋ 프로세스

  • 상태를 갖음

  • 최소한 하나의 스레드를 포함

스레드
  • 실행 스레드는 프로세스가 실행할 일련의 명령을 포함

  • 대게 GUI 어플리키에션에서 메인 스레드, UI 스레드 등이 있음

코루틴
  • 코틀린이 동싱성을 구현한 방식을 보면 개발자가 직접 스레드를 시작하거나 중지할 필요없다는 것을 알게 됨

  • 코루틴coroutin을 실행하도록 지시하기만 하면 됨

  • 스레드와 관련된 나머지 처리는 프레임워크에 의해 수행 됨

코루틴

  • 코틀린 문서에서는 코루틴은 경량 스레드라고 함

    • 스레드처럼 프로세서가 실행할 명령어 집합의 실행을 정의하기 때문

    • 스레드와 비슷한 라이프 사이클을 가짐

  • 코루틴은 스레드 안에서 실행됨

  • 코루틴이 특정 스레드에서 실행되더라도 스레드에 묶이지 않음

    • 코루틴의 일부를 특정 스레드에서 실행하고, 실행 중지한 뒤 나중에 다른 스레드에서 계속 실행하는 것이 가능

동시성은 병렬성이 아니다

  • 병렬 실행paralled execution은 두 스레드가 정확히 같은 시점에 실행될 때만 발행

  • 동시 실행concurrent execution은 단일 코어가 서로 다른 스레드의 인스트럭션instructions, 명령을 교차 배치해서, 스레드들의 실행을 효율적으로 겹쳐서 실행

    • Context switch 발생

  • 병렬은 동시성을 의미하지만 동시성은 병렬성이 없이도 발생할 수 있다는 점에 유의

    • 분산 컴퓨팅(distributed computing)은 병렬성의 한 형태

Tip

동시성 코드가 항상 필요하다거나 꼭 이득을 얻는 것이 아님. 코드의 병목과 스레드 및 코루틴의 작동방식과 동시성/병렬성 차이를 이해해 두면 언제 어떻게 구현해야 하는지 판단 가능

CPU 바운드와 I/O 바운드

  • CPU 바운드 알고리즘의 경우 다중 코어에서 병렬성을 활용하면 성능을 향상시킬 수 있음

    • but, 단일 코어에서 동시성을 구현하면 성능이 저하되기도 함

      • 예를 들어, 컨텍스트 스위칭으로 인한 오버헤드

      • Context switching은 현재 스레드의 상태를 저장한 후 다음 스레드의 상태를 적재해야하기 때문에 전체 프로세스의 오버헤드가 발생

    • CPU 바운드 알고리즘을 위해서는 현재 사용중인 장치의 코어 수를 기준으로 적절한 스레드 수 생성을 고려해야 함

      • CPU 바운드 알고리즘을 실행하기 위해 생성된 스레드 풀인 코틀린의 CommonPool 을 활용할 수 있음 (CommonPool 크이는 머신의 코어 수에서 1을 뺀 값)

  • I/O 작업은 동시성으로 실행하는 편이 좋음

    • I/O 바운드 알고리즘은 끊임없이 무언가를 기다림

    • 단일 코어 기기에서 대기하는 중에 다른 프로세스를 사용할 수 있게 함

동시성이 어려운 이유

  • 동시성 프로그램 자체가 어렵기도 하지만 많은 언어가 이것을 더 어렵게 만들기 때문

  • 동시성 코드를 작성할 때 제시되는 공통된 문제점 고려해야 함

레이스 컨디션Race conditions: 경합 조건

  • 동시성 코드를 작성할 때 가장 흔한 오류

  • 코드를 동시성으로 작성했지만 순차적 코드처럼 동작할 것이라고 예상할 때 발생

  • 동시성 코드가 항상 특정한 순서로 실행될 것이라 가정하고 오해할 때 생기는 문제

  • 정보에 접근하려고 하기 전에 정보를 얻을 때까지 명시적으로 기다리도록 해야 함

Example
data class UserInfo(val name: String, val lastName: String, val id: Int)

lateinit var user: UserInfo

fun main(args: Array<String>) = runBlocking {
    asyncGetUserInfo(1)
    // Do some other operations
    delay(1000)

    println("User ${user.id} is ${user.name}") // (1)
}

fun asyncGetUserInfo(id: Int) = async {
    user = UserInfo(id = id, name = "Susan", lastName = "Calvin")
}
  1. 순차적으로 실행될 것이라고 예상하고 user 에 접근. 명시적으로 asyncGetUserInfo 함수를 기다도록 해야 함

원자성 위반atomic operations

  • 작업이 사용하는 데이터를 간섭 없이 접근할 수 있음을 말함

  • 단일 스레드 어플리케이션에서는 모든 코드가 순차적으로 실행되기 때문에 모든 작업이 모두 원자일 것

  • 원자성은 객체의 상태가 동시에 수정될 수 있을 때 필요, 상태의 수정이 겹치지 않도록 보장해야 함

Example
var counter = 0 // 원자성 위반
fun main(args: Array<String>) = runBlocking {
    val workerA = asyncIncrement(2000)
    val workerB = asyncIncrement(100)
    workerA.await()
    workerB.await()
    println("counter [$counter]")
}

fun asyncIncrement(by: Int) = GlobalScope.async {
    for(i in 0 until by) {
        counter++
    }
}

교착 상태circular dependencies

  • 두 작업이 서로를 기다리고 있을 때 누구도 끝나지 않는 상황

  • 실제 시나리오에서 교착 상태를 발견하고 수정하기란 어려움

  • 레이스 컨디션은 교착 상태가 발생할 수 있는 예기치 않은 상태를 만들기도 함

라이브 락Livelocks

  • 교착 상태와 유사

  • 라이브 락이 진행될 때 어플리케이션의 상태를 지속적으로 변하지만 정상 실행으로 돌아오지 못하게 하는 방향으로 상태가 변경됨

    • 예를 들면, 두 사람이 복도를 지나갈 때 서로를 피하기 위해 왼쪽, 오른쪽으로 움직이지만 계속 서로의 길을 막게 되는 상황

  • 교착 상태를 복하도록 설계된 알고리즘에서 라이브 락이 발생하는 경우가 많음

코틀린에서의 동시성

  • 코틀린은 동시성에 대해 현대적이고 신선한 접근 방식을 취함. 코틀린을 사용하면 넌 블로킹이며, 가독성 있게 활용될 뿐만 아니라 유연한 동시성 코드를 작성할 수 있음

넌 블로킹

  • 스레드는 무겁고 생성하는데 비용이 많이 들며 제한된 수의 스레드만 생성할 수 있음

    • 스레드가 블로킹되면 어떻게 보면 자원이 낭비되는 셈

  • 코틀린에서는 중단 가능한 연산Suspendable Computations이라는 기능을 제공함

    • 스레드의 실행을 블로킹하지 않으면서 실행을 잠시 중단하는 것

  • 코틀린은 채널channels, 액터actors, 상호 배제mutual exclustions와 같은 훌륭한 기본형도 제공해 스레드를 블록하지 않고 동시성 코드를 효과적으로 통신하고 동기화하는 메커니즘 제공

명시적인 선언

Tip

관례상 기본적으로 동시에 실행될 함수는 async로 시작하거나 Async로 끝나도록 이름을 짓도록 함

Note

비동기 함수를 작성하는 대신 suspend 함수를 작성해 async 또는 lanuch 블럭 안에서 호출하는 것이 좋다. suspend 함수를 갖게 되면 함수의 호출자에게 더 많은 유연성을 제공하기 때문이다. 가령 호출자가 언제 동시적으로 실행할 것인지를 결정할 수 있다.

  • 동시성은 싶은 고민과 설계가 필요해, 연산이 동시에 실행되야 하는 시점을 명시적으로 만드는 것이 중요함

  • 코틀린의 동시성 코드는 순차적 코드만큼 읽기 쉬움 → 가독성

    suspend fun getProfile(id: Int) {
        val basicUserInfo = getUserInfoAsync(id)
        val contactInfo = getContactInfoAsync(id)
    
        createProfile(basicUserInfo.await(), contactInfo.await())
    }
  • 코틀린은 동시성 코드를 쉽게 구현할 수 있는 고급 함수와 기본형 제공

    • newSingleThreadContext(): 스레드 이름을 파라미터로 하는 스레드 생성 함수

    • newFixedThreadPoolContext(): 크기와 이름을 파라미터로 하는 스레드 풀 생성 함수

    • CommonPool: CPU 바운드 작업에 최적인 스레드 풀

    • 코루틴을 다른 스레드로 이동시키는 역할은 런타임이 담당

    • 채널, 뮤텍스 및 스레드 한정과 같은 코루틴의 통신과 동기화를 위해 필요한 많은 기본형과 기술이 제공됨

  • 코틀린은 간단하면서도 유연하게 동시성을 사용하게 해주는 기본형 제공

    • 채널Channels: 코루틴 간에 데이터를 안전하게 보내고 받는 데 사용할 수 있는 파이프

    • 작업자 풀Worker pools: 많은 스레드에서 연산 집합의 처리를 나눌 수 있는 코루틴의 풀

    • 액터Actors: 채널과 코루틴을 사용하는 상태를 감싼 래퍼. 여러 스레드에서 상태를 안전하게 수정하는 메커니즘 제공

    • 뮤텍스Mutexes: 크리티컬 존 영역을 정의해 한번에 하나의 스레드만 실행할 수 있도록 하는 동기화 메커니즘

    • 스레드 한정Thread confinement: 코루틴의 실행을 제한해서 지정된 스레드에서만 실행하도록 하는 기능

    • 생성자(반복자 및 시퀀스): 필요에 따라 정보를 생성할 수 있고 새로운 정보가 필요하지 않을 때 일시 중단될 수 있는 데이터 소스

코틀린 동시성 관련 개념과 용어

일시 중단 연산Suspending computations

TODO

일시 중단 함수

람다 일시 중단

코루틴 디스패처

코루틴 빌더