티스토리 뷰

ff96b901-18fc-4994-9b83-675fd0663366

start Date: 2024-05-21

end Date: 2024-06-09

책의 소개

이 책은 안드로이드 중심으로 설명하고 있는 책입니다.

프로세스, 스레드, 코루틴

애플리케이션을 실행할 때 OS는 프로세스를 생성하고 여기에 스레드를 연결한 다음, 메인 스레드를 시작합니다.

프로세스

실행 중인 애플리케이션의 인스턴스입니다.

애플리케이션이 시작될 때마다 애플리케이션의 프로세스가 시작됩니다.

스레드

실행 스레드는 프로세스가 실행할 일련의 명령을 포함합니다.

따라서 프로세스는 최소한 하나의 스레드를 포함하며 이 스레드는 애플리케이션의 진입점(entry point)을 실행하기 위해 생성됩니다.

보통 진입점은 애플리케이션의 main() 함수이며 메인 스레드라 하는데 프로세스의 라이프 사이클과 밀접하게 연관됩니다.

스레드가 끝나면 프로세스의 다른 스레드와 상관없이 프로세스가 종료됩니다.

코루틴이 동시성을 구현한 방식을 보면 우리가 직접 스레드를 시작하거나 중지할 필요가 없다는 것을 알 게 됩니다.

한 두줄의 코드로 코틀린이 특정 스레드나 스레드 풀을 생성해서 코루틴을 실행하도록 지시하기만 하면 됩니다.

코루틴

코루틴은 경량 스레드라고도 합니다.

대부분의 스레드와 마찬가지로 코루틴이 프로세서가 실행할 명령어 집합의 실행을 정의하기 때문입니다.

또한 코루틴은 스레드와 비슷한 라이프 사이클을 가지고 있습니다.

코루틴은 스레드 안에서 실행됩니다.

스레드 하나에 많은 코루틴이 있을 수 있지만 주어진 시간에 하나의 스레드에서 하나의 명령만이 실행될 수 있습니다.

즉 같은 스레드에 10개의 코루틴이 있다면 어느 한 시점에는 하나의 코루틴만 실행됩니다.

스레드 vs 코루틴

코루틴이 스레드보다 빠르고 적은 비용으로 생성할 수 있다는 것입니다.

suspend fun createCoroutines(amount: Int) {
    val jobs = ArrayList<Job>()
    for (i in 1..amount) {
        jobs += launch {
            delay(1000)
        }
    }
    jobs.forEach {
        it.join()
    }
}

위 함수는 파라미터 amount만큼 코루틴을 생성해 각 코루틴을 1초간 지연시킨 후 모든 코루틴이 종료될 때까지 기다렸다가 반환합니다.

amount가 10,000으로 실행할 때 1.160ms, 100,000으로 실행할 때 1,649ms가 소요됐다고 합니다.

코틀린은 고정된 크기의 스레드 풀을 사용하고 코루틴을 스레드들에 배포하기 때문에 실행 시간이 매우 적게 증가합니다.

코루틴이 일시 중단되는 동안(delay()가 실행되는 동안) 실행 중인 스레드는 다른 코루틴을 실행하는 데 사용되며 코루틴은 시작 혹은 재개될 준비 상태가 됩니다.

👨🏼‍💻 Thread 클래스의 activeCount() 메서드는 활성화된 스레드 수를 알 수 있습니다.
fun main(args: Array<String>) = runBlocking {
    println("${Thread.activeCount()} threads active at the start")

    val time = measureTimeMillis {
        createCoroutines(10000)
    }
    println("${Thread.activeCount()} threads active at the end")
    println("Took $time ms")
}


suspend fun createCoroutines(amount: Int) {
    val jobs = ArrayList<Job>()
    for (i in 1..amount) {
        jobs += CoroutineScope(Dispatchers.IO).launch() {
            delay(1000)
        }
    }
    jobs.forEach {
        it.join()
    }
}

코루틴이 특정 스레드 안에서 실행되더라도 스레드와 묶이지 않는다는 점을 이해해야 합니다.

코뤁니의 일부를 특정 스레드에서 실행하고, 실행을 중지한 다음 나중에 다른 스레드에서 계속 실행하는 것이 가능합니다.

코틀린이 실행 가능한 스레드로 코루틴을 이동시키기 때문입니다.

fun main(args: Array<String>) = runBlocking {
    println("${Thread.activeCount()} threads active at the start")

    val time = measureTimeMillis {
        createCoroutines(3)
    }
    println("${Thread.activeCount()} threads active at the end")
    println("Took $time ms")
}


suspend fun createCoroutines(amount: Int) {
    val jobs = ArrayList<Job>()
    for (i in 1..amount) {
        jobs += CoroutineScope(Dispatchers.IO).launch() {
            println("Started $i in ${Thread.currentThread().name}")
            delay(1000)
            println("Finished $i in ${Thread.currentThread().name}")
        }
    }
    jobs.forEach {
        it.join()
    }
}

프로세스, 스레드, 코루틴 결론

애플리케이션은 하나 이상의 프로세스로 구성되어 있고 각 프로세스가 하나 이상의 스레드를 가집니다.

스레드를 블록한다는 것은 그 스레드에서 코드이 실행을 중지한다는 의미인데, 사용자와 상호작용하는 스레드는 블록되지 않아야 합니다.

코루틴이 기본적으로 스레드 안에 존재하지만 스레드에 얽매이지 않은 가벼운 스레드입니다.

동시성은 애플리케이션이 동시에 한 개 이상의 스레드에서 실행될 때 발생합니다.

동시성이 발생하려면 두 개 이상의 스레드가 생성돼야 하며, 애플리케이션이 제대로 작동하려면 이런 스레드 간의 통신과 동기화가 필요합니다.

동시성

올바른 동시성 코드는 언제나 똑같은 과정을 거쳐서 항상 똑같은 결과를 내놓지만, 실행 순서에서는 약간의 가변성을 허용하는 코드입니다.

그러려면 코드의 서로 다른 부분이 어느 정도 독립성이 있어야 하며 약간의 조정도 필요합니다.

동시성을 이해하는 가장 좋은 방법은 순차적인 코드를 동시성 코드와 비교하는 것입니다.

다음은 비동시성 코드입니다.

fun getProfile(id: Int) : Profile {
    val basicUserInfo = getUserInfo(id) // 사용자 정보
    val contactInfo = getContactInfo(id) // 연락처 정보
    
    return createProfile(basicUserInfo, contactInfo)
 }

이 코드에서 중요한 점은 사용자 정보가 반환되기 전까지는 연락처 정보를 요청하지 않는다는 점입니다.

이것이 순차 코드의 장점입니다.

정확한 실행 순서를 쉽게 알 수 있어서 예측하지 못한 일이 벌어지지는 않습니다.

하지만 위 코드의 단점은 다음과 같습니다.

  • 동시성 코드에 비해 성능이 저하될 수 있음
  • 코드가 실행되는 하드웨어를 제대로 활용하지 못할 수 있음

다음은 동시성 코드입니다.

suspend fun getProfile(id: Int) : Profile {
    val basicUserInfo = asyncGetUserInfo(id) // 사용자 정보
    val contactInfo = asyncGetContactInfo(id) // 연락처 정보
    
    return createProfile(basicUserInfo.await(), contactInfo.await())
 }

위 코드는 suspend function(일시 중단 함수)로서 정의에 suspend 수식어가 있음을 알 수 있으며, 위 두 메서드는 비동기로 구현되어 있습니다.

두 메서드는 서로 다른 스레드에서 실행되도록 작성됐기 때문에 동시성이라고 합니다.

await()이 있는 이유

await()이 하는 일은 두 메서드가 모두 완료할 때까지 getProfile()의 실행을 일시 중단하는 것입니다.

두 메서드가 완료되었을 때만 createProfile()이 실행됩니다.

이렇게 되면 어떤 동시성 호출이 먼저 종료되는지에 관계없이 getProfile()의 결과가 결정론적임을 보장합니다.

심지어 동시성 코드를 코루틴 간에 통신하게 하도록 조정할 수 있습니다.

동시성은 병렬성이 아니다

두 개의 코드가 동시에 실행된다는 점에서 병렬적으로 보이기도 합니다.

동시성

동시성은 두 개 이상의 알고리즘의 실행 시간이 겹쳐질 때 발생합니다.

중첩이 발생하려면 두 개 이상의 실행 스레드가 필요합니다.

이런 스레드들이 단일 코어에서 실행되면 병렬이 아니라 동시에 실행되는데, 단일 코어가 서로 다른 스레드의 인스트럭션을 교차 배치해서 스레드들의 실행을 효율적으로 겹쳐서 실행합니다.

병렬성

병렬은 두 개의 알고리즘이 정호가히 같은 시점에 실행될 때 발생합니다.

이것이 가능하려면 2개 이상의 코어와 2개 이상의 스레드가 있어야 각 코어가 동시에 스레드의 인스트럭션을 실행할 수 있습니다.

병렬은 동시성을 의미하지만, 동시성은 병렬성 없이도 발생할 수 있다는 점을 명심합시다.

CPU 바운드와 I/O 바운드

병목 현상은 다양한 유형의 성능저하가 발생하는 지점을 나타냅니다.

애플리케이션의 성능을 최적화할 때 가장 중요한 사항입니다.

CPU 바운드

CPU만 완료되면 작업을 중심으로 구현되는 알고리즘이 많습니다.

웬만한 코틀린 코드가 이에 해당합니다.

I/O 바운드

입출력 장치에 의존하는 알고리즘입니다.

예시로 파일을 읽는 코드(readWordsFromJson())이 있습니다.

네트워킹이나 컴퓨터 주변기기로부터의 입력을 받는 작업들도 I/O 작업에 해당합니다.

CPU 바운드 알고리즘에서의 동시성과 병렬성

CPU 바운드 알고리즘의 경우 다중 코어에서 병렬성을 활용하면 성능을 향상시키지만 단일 코어에서 동시성을 구현하면 성능이 저하되기도 합니다.

단일 코어에서 실행한다면 하나의 코어가 3개의 스레드 사이에서 교차 배치(interleave)되며 매번 일정량의 단어를 필터링하고 다음 스레드로 전환됩니다.

전환 프로세스를 컨텍스트 스위칭이라고 합니다.

CPU 바운드 알고리즘을 위해서는 현재 사용 중인 장치의 코어 수를 기준으로 적절한 스레드 수를 생성하도록 고려해야만 합니다.

I/O 바운드 알고리즘에서의 동시성과 병렬성

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

지속적인 대기는 단일 코어 기기에서 대기하는 중에 다른 유용한 작업에 프로세스를 사용할 수 있도록 합니다.

따라서 I/O 바운드인 동시성 알고리즘은 병렬이거나 단일 코어에 상관없이 유사하게 수행됩니다.

I/O 작업은 늘 동시성으로 실행하는 편이 좋습니다.

코틀린에서의 동시성

넌 블로킹

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

스레드가 블로킹되면 자원이 낭비되는 셈이어서 코틀린은 중단 가능한 연산이라는 기능을 제공합니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
글 보관함