728x90
코틀린 동시성 프로그래밍을 요약한 내용입니다.
프로세스, 스레드, 코루틴
프로세스
- 프로세스는 실행 중인 애플리케이션의 인스턴스
- 프로세스는 상태를 갖고 있다
- 핸들, 프로세스 ID, 데이터, 네트워크 등의 상태
- 프로세스 내부의 스레드가 액세스 할 수 있다
- 애플리케이션은 여러 프로세스로 구성될 수 있다
- 브러우저 같은 경우 여러 프로세스로 구성
스레드
- 실행 스레드는 프로세스가 실행할 일련의 명령을 포함
- 프로세스는 최소한 하나의 스레드를 포함
- 애플리케이션의 진입정을 실행하기 위해 생성
- 애플리케이션이 실행되면 main() 함수의 명령 집합이 포함된 메인 스레드가 생성
- doWork는 메인 스레드에서 실행되므로 doWork() 종료되면 애플리케이션의 실행이 종료
fun main(args: Array<String>) { doWork() }
- 스레드가 속한 프로세스에 포함된 리소스를 액세스 가능
- 스레드는 로컬 스토리지라는 자체저장소도 갖고 있다
- 스레드 안에서 명령은 한 번에 하나씩 실행돼 스레드가 블록되면 블록이 끝날 때까지 같은 스레드에서 다른 명령을 실행할 수 없다
- GUI 애플리케이션에는 UI 스레드가 있다
- UI 스레드는 사용자 인터페이스를 업데이트하고 사용자와 애플리케이션 간의 상호작용을 리스링 한다
- 스레드를 블록하면 애플리케이션이 UI를 업데이트하거나 사용자로부터 상호작용을 수신하지 못하도록 방해
코루틴
- 코루틴을 경량 스레드라고도 한다
- 스레드와 마찬가지로 코루틴이 프로세스가 실행할 명령어 집합의 실행을 정의
- 코루틴은 스레드와 비슷한 라이프 사이클을 갖고 있다
- 코루틴은 스레드 안에서 실행
- 스레드 하나에 많은 코루틴이 있을 수 있음
- 주어진 시간에 하나의 스레드에서 하나의 명령만 실행 가능
- 스레드와 코루틴의 가장 큰 차이점은 코루틴이 빠르고 적은 비용으로 생성할 있다
- 함수는 amount에 지정된 수 만큼 코루틴을 생성해 각 코루틴을 1초가 지연시킨 후 모든 코루틴이 종료될 때까지 기다렸다가 반환
- amount를 10,000 실행 시: 약 1,160ms
- amount를 100,000 실행 시 : 약 1649ms
- 코틀린은 고정된 크기의 스레드 풀을 사용하고 코루틴을 스레드들에게 배포하기 때문에 실행 시간이 매우 적게 증가
- 수천 개의 코루틴을 추가하는 것은 거의 영향이 없다
- 코루틴이 일시 중단되는 동안 실행 중인 스레드는 다른 코루틴을 실행하는 데 사용
suspend fun createCoroutines(amount: Int) { val jobs = ArrayList<job>() for (i in 1..amount) { jobs += launch { delay(1000) } } jobs.forEach { it.join() } } fun main(args: Array<String>) = runBlocking { val time = measureTimeMillis { createCoroutines(10_000) } println("Took $time ms") }
<aside> 💡 measureTimeMillis는 코드 블록을 갖는 인라인 함수이며 실행 시간을 밀리초로 반환
</aside>
- Thread 클래스의 activeCount() 메소드를 활용하면 활성화된 스레드 수를 알수 있다
- fun main(args: Array<String>) = runBlocking { println("${Thread.activeCount()} threads active at the start") val time = measureTimeMillis { createCoroutines(10_000) } println("${Thread.activeCount()} threads active at th end") println("Too $time ms") }
- 코루틴이 특정 스레드 안에서 실행되더라도 스레드와 묶이지 않는다는 점을 이해해야 한다
- 코루틴의 일부를 특정 스레드에서 실행
- 실행을 중지한 다음 나중에 다른 스레드에서 계속 실행 가능
suspend fun createCoroutines(amount: Int) { val jobs = ArrayList<job>() for (i in 1..amount) { jobs += launch { println("Start $i in ${Thread.currentThread().name}") delay(1000) println("Finished $i in ${Thread.currentThread().name}") } } jobs.forEach { it.join() } }
- 스레드는 한 번에 하나의 코루틴만 실행할 수 있기 때문에 프레임워크가 필요에 따라 코루틴을 스레드들 사이에 옮기는 역활을 한다
- 코틀린은 개발자가 코루틴을 실행할 스레드를 지정하거나 코루틴을 해당 스레드로 제한할지 여부를 지정할 수 있을 만큼 충분히 유연
Sync
동시성에 대해
- 올바른 동시성 코드는 결정론적인 결괄르 갖지만 실행 순서에서는 약간의 가변성을 허용
- 비동시성 코드
- 사용자 정보가 반환되기 전까지 연락처 정보를 요청하지 않음
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()) }
동시성은 병렬성이 아니다
- 동시성과 병렬성을 혼동하곤 한다
- 두 개의 코드가 동시에 실행된다는 점에서 비슷해 보이긴 한다
- 동시성 코드
- asyncGetUserInfo와 asyncGetContactInfo는 중복 실행
- createProfile은 중복 실행되지 않음
suspend fun getProfile(id: Int): Profile { val basicUserInfo = asyncGetUserInfo(id) val contactInfo = asyncGetContactInfo(id) return createProfile(basicUserInfo.await(), contactInfo.await()) }
- getProfile 코드를 코어가 하나만 있는 기계에서 실행할 경우
- 단일 코어는 두 스레드를 동시에 실행 할 수 없음
- asyncGetUserInfo와 asyncGetContactInfo 간에 교차 배치되 스케줄이 겹치지만 동시에 실행되지 않음
- 병렬 실행은 두 스레드가 정확히 같은 시점에 실행될 때만 발생한다
- 두 개의 코어가 있는 컴퓨터에서 getProfile이 실행되고 있는 경우
- 코어 하나는 asyncGetUserInfo 실행
- 다른 코어는 asyncGetContactInfo 실행
- 스레드 타임라인이 겹칠 뿐만 아니라 정확히 같은 시점에 실행된다
- 동시성은 두 개 이상의 알고리즘의 실행 시간이 겹쳐질 때 발생한다
- 중첩이 발생하려면 두 개 이상의 실행 스레드가 필요
- 스레드들이 단일 코어에서 실행되면 병렬이 아니라 동시에 실행
- 단일 코어가 서로 다른 스레드의 인스트럭션을 교차 배치해서 효율적으로 실행
- 병렬은 두 개의 알고리즘이 정확히 같은 시점에 실행될 때 발생
- 2개 이상의 코어와 2개 이상의 스레드가 있어야 각 코어가 동시에 스레드의 인스트럭션을 실행
- 병렬은 동시성을 의미하지만 동시성은 병렬성이 없어도 발생할 수 있다는 점에 유의
코틀린에서의 동시성
넌 블로킹
- 스레드는 무겁고 생성하는데 비용이 많이 들며 제한된 수의 스레드만 생성할 수 있다.
- 스레드가 블록킹되면 자원이 낭비되는 셈이여서 코틀린은 중단 가능한 연산이라는 기능을 제공
- 스레드의 실행을 블록킹하지 않으면서 실행을 잠시 중단 하는 것
명시적인 선언
- 동시성은 깊은 고민과 설계가 필요
- 연산이 동시에 실행돼야 하는 시점을 명시적으로 만드는 것이 중요
- 일시 중단 가능한 연산은 기본적으로 순차적으로 실행
- 연산은 일시 중단될 때 스레드를 블로킹하지 않기 때문에 직접적인 단점은 아님
- getName, getLastName을 순차적으로 실행
- fun main(args: Array<String>) = runBlocking { val time = mesureTimeMillis { val name = getName() val lastName = getLastName() println("Hello, $name $lastName") } println("Excution took $time ms") } suspend fun getName(): String { delay(1000) return "Susan" } suspend fun getLastName(): String { delay(1000) return "Calvin" }
- getName, getLastName을 동시에 수행
- 두 함수를 동시에 실행하며 await을 호출해 두연산에 모두 결과가 나타날 때까지 main이 일시 중단되도록 요청
fun main(args: Array<String>) = runBlocking { val time = mesureTimeMillis { val name = async { getName() } val lastName = async { getLastName() } println("Hello, ${name.await()} ${lastName.await()}") } println("Excution took $time ms") } suspend fun getName(): String { delay(1000) return "Susan" } suspend fun getLastName(): String { delay(1000) return "Calvin" }
가독성
- 코틀린의 동시성 코드는 순차적 코드만큼 읽기 쉽다
- 코틀린의 접근법은 관용구적인 동시성 코드를 허용
- suspend 메소드는 백그라운드 스레드에서 실행될 두 메소드를 호출하고 정보를 처리하기 전에 완료를 기다린다
- suspend fun getProfile(id: Int): Profile { val basicUserInfo = asyncGetUserInfo(id) val contactInfo = asyncGetContactInfo(id) return createProfile(basicUserInfo.await(), contactInfo.await()) }
기본형 활용
- 스레드를 만들고 관리하는 것은 동시성 코드를 작성할 때 가장 어려운 부분 중 하나
- 언제 스레드를 만들 것인가를 아는 것 못지 않게 얼마나 많은 스레드를 만드는지를 아는 것도 중요
- 스레드를 통신/동기화 하는 것은 그 자체로 어려운일
- 코틀린은 동시성 코드를 쉽게 구현할 수 있는 고급 함수와 기본형을 제공
- 스레드는 스레드 이름을 파라미터로 하는 newSingleThreadContext()를 호출하면 생성
- 생성되면 필요한 만큼 많은 코루틴을 수행하는데 사용
- 스레드 풀은 크기와 이름을 파라미터로 하는 newFixedThreadPoolContet()를 호출하면 생성
- CommonPool은 CPU 바운드 작업에 최적인 스레드 풀이다.
- 최대 크기는 시스템 코어에서 1을 뺀 값
- 코루틴을 다른 스레드로 이동시키는 역활은 런타임이 담당
- 채널, 뮤텍스 및 스레드 한정과 같은 코루틴의 통신과 동기화를 위해 필요한 많은 기본형과 기술이 제공
- 스레드는 스레드 이름을 파라미터로 하는 newSingleThreadContext()를 호출하면 생성
일시 중단 연산
- 일시 중단 연산은 해당 스레드를 차단하지 않고 실행을 일시 중지할 수 있는 연산
- 스레드를 차단하는 것은 불편하기 때문에 자체 실행을 일시 중단하면 일시 중단 연산을 통해 스레드를 다시 시작해야 할 때까지 스레드를 다른 연산에서 사용 가능
일시 중단 함수
- 일시 중단 함수는 suspend 제어자 때문에 쉽게 식별 가능
- suspend fun greetAfter(name: String, delayMillis: Long) { delay(delayMillis) println("Hello, $name") }
람다 일시 중단
- 일반적인 람다와 마찬가지로, 일시 중단 람다는 익명의 로컬 함수
- 다른 일시 중단 함수를 호출함으로써 자신의 실행을 중단할 수 있다는 점에서 보통의 람다와 차이가 있다
코루틴 디스패처
- 코루틴을 시작하거나 재개할 스레드를 결정하기 위해 코루틴 디스패처가 사용된다.
- 모든 코루틴 디스패처는 CoroutinDispatcher 인터페이스를 구현해야 함
- DefaultDispatcher
- CommonPool과 같다
- CommonPool
- 공유된 백그라운드 스레드 풀에서 코루틴을 실행하고 다시 시작한다
- 기본 크기는 CPU 바운드 작업에서 사용하기에 적합
- Unconfied
- 현재 스레드에서 코루틴을 시작하지만 어떤 스레드에서도 다시 재개될 수 있다.
- 디스패처에서는 스레드 정책을 사용하지 않는다
- DefaultDispatcher
- 디스패처와 함께 필요에 따라 풀 또는 스레드의 정의하는 데 사용할 수 있는 빌더
- newSingleThreadContext()
- 단일 스레드로 디스패처를 생성
- 실행되는 코루틴은 항상 같은 스레드에서 시작되고 재개된다.
- newFixedThreadPoolContext()
- 지정된 크기의 스레드 풀이 있는 디스패처를 만든다
- 런타임은 디스패처에서 실행된 코루틴을 시작하고 재개할 스레드를 결정
- newSingleThreadContext()
코루틴 빌더
- 일시 중단 람다를 받아 실행시키는 코루틴을 생성 하는 함수
- 다양한 시나리오에 맞게 활용할 수 있는 코루틴 빌더를 제공
- async()
- 결과가 예상되는 코루틴을 시작하는 데 사용
- 코루틴 내부에서 일어나는 모든 예외를 캡처해서 결과에 넣기 때문에 조심해서 사용
- 결과 또는 예외를 포함하는 Deferrd<T>를 반환
- launch()
- 결과가 반환하지 않는 코루틴을 시작
- 자체 혹은 자식 코루틴의 실행을 취소하기 위해 사용할 수 있는 Job을 반환
- runBlocking()
- 블로킹 코드를 일시 중지 가능한 코드로 연결하기 위해 작성
- 보통 main 메소드와 유잇 테스트에서 사용 된다.
- runBlocking은 코루틴의 시행이 끝날 때까지 현재 스레드를 차단
- async()
- async() 예제
- val result = GlobalScope.async { isPalindrome(word = "Sample") } result.await()
- 디스패처를 수동으로 지정
- val result = GlobalScope.async(Dispatchers.Unconfined) { isPalindrome(word = "Sample") } result.await()
728x90
'코틀린 스터디' 카테고리의 다른 글
채널 (0) | 2023.12.04 |
---|---|
코루틴 스코프 만들기 (1) | 2023.11.24 |
코틀린 - 예외처리, Type System, 컬렉션 (0) | 2023.11.10 |
코틀린 - 클래스, 인터페이스, 상속 (0) | 2023.11.10 |
코틀린 - 변수, 연산자, 반복문, 함수 (1) | 2023.11.10 |