이 포스팅은 <코틀린 동시성 프로그래밍>, 미구엘 엔젤 카스티블랑코 토레스, 에이콘출판사(2020)을 읽고 개인 학습용으로 정리한 글입니다.
프로세스: 실행 중인 애플리케이션의 인스턴스
애플리케이션이 시작될 때마다 프로세스가 시작된다
프로세스는 상태를 갖고 있다
애플리케이션은 여러 프로세스로 구성될 수 있다
실행 스레드는 프로세스가 실행할 일련의 명령을 포함
프로세스는 최소한 하나의 스레드 포함
-> 메인 스레드
-> 이 스레드는 애플리케이션의 진입점(entry)을 실행하기 위해 생성됨
-> 보통 진입점은 애플리케이션의 main() 함수
메인 스레드는 프로세스의 라이프 사이클과 밀접하게 연관
-> 메인 스레드가 끝나면 프로세스의 다른 스레드와 상관없이 프로세스 종료
fun main(args: Array<String>){
doWork()
}
스레드 안에서 명령은 한 번에 하나씩 실행
스레드가 block되면 블록이 끝날 때까지 같은 스레드 내에서 다른 명령 실행 X
-> 어플리케이션 UX에 부정적 영향을 미칠 수 있는 스레드 블로킹 X
-> 블로킹 작업을 별도의 전용 스레드에 할당해야
그래픽 사용자 인터페이스(GUI) 어플리케이션은 UI 스레드를 가짐
-> UI스레드: 사용자 인터페이스 업데이트, 사용자와 어플리케이션 간의 상호작용 리스닝
-> 어플리케이션의 응답성을 항상 유지하기 위해 UI 스레드 블록 X
안드로이드 3.0 이상에서는 UI 스레드에서 네트워킹 작업을 하면 어플리케이션 중단
-> 네트워킹 작업은 스레드를 중단시킬 수 있기 때문
코루틴(coroutine): 경량 스레드
-> 프로세서가 실행할 명령어 집합의 실행 정의
-> 스레드와 비슷한 라이프사이클
코루틴은 스레드 안에서 실행된다
-> 스레드 하나에 많은 코루틴이 있을 수 있음
-> 주어진 시간에 하나의 스레드에서 하나의 코루틴만 실행됨
스레드와 코루틴의 차이점:
코루틴은 빠르고 적은 비용으로 생성할 수 있다
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 = measureMillis{
createCoroutines(10000)
}
println("Took $time ms")
}
measureMillis(): 코드 블록을 갖는 인라인 함수
-> 실행 시간을 밀리초(ms)로 반환
-> 코드의 실행 시간을 대략적으로 예측할 때 유용
테스트 환경에서
amount = 10,000 -> 1160ms 소요
amount = 100,000 -> 1649ms 소요
코틀린은 고정된 크기의 스레드 풀 사용 & 코루틴들을 스레드에 배포
-> 실행 시간 매우 적게 증가
코루틴이 일시 중단되는 동안(delay() 호출) 실행 중인 스레드는 다른 코루틴은 실행하는 데 사용됨
동시성: 어플리케이션이 동시에 한 개 이상의 스레드에서 실행될 때 발생
-> 두 개 이상의 스레드 생성되어야 동시성 발생
-> 이런 스레드 간의 통신과 동기화 필요
올바른 동시성 코드:
결정론적인 결과를 갖지만 실행 순서에서는 약간의 가변성 허용
-> 코드의 서로 다른 부분이 어느정도 독립성이 있어야
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())
}
getProflie()은 일시 중단 함수(suspend function)
asyncGetUserInfo(), asyncGetContactInfo() 비동기(asynchronous)로 구현
-> 서로 다른 스레드에서 동시에 실행된다고 가정
-> 동시성 발생
createProfile()를 호출할 때 두 개의 await() 호출
-> asyncGetUserInfo(), asyncGetContactInfo()가 모두 완료될 때까지 createProfile()의 실행 일시 중단
-> 어떤 동시성 호출이 먼저 종료되는지에 관계없이 결과 결정론적
asyncGetUserInfo(), asyncGetContactInfo() 중복 실행된다
동시성 코드가 까다로운 이유:
코드의 준독립적인(semi-independent)부분이 완성되는 순서에 관계없이 결과가 결정론적임을 보장해야
병렬적 실행을 위한 타임라인은 동시성 타인라인과 같아 보일 것
같은 프로세스 안에서 서로 다른 명령 집합의 타임라인이 겹칠 때 동시성 발생
-> 정확히 같은 시점에 실행되는지 여부와 상관 X
병렬 실행(paralled execution)은 두 스레드가 정확히 같은 시점에 실행될 때만 발생
동시성은 두 개 이상의 알고리즘의 실행시간이 겹칠 때 발생
-> 중첩 가능하려면 두 개 이상의 실행 스레드 필요
-> 단일 코어에서 실행되면 서로 다른 스레드의 instructions 교차 배치 -> 병렬이 아니라 동시에(concurrently)
병렬은 두 개의 알고리즘이 정확히 같은 시점에 실행될 때 발생
-> 두 개 이상의 코어와 두 개이상의 스레드 필요
-> 각 코어가 동시에 스레드의 instruction 실행
병렬은 동시성을 의미, 동시성은 병렬 없이도 발생 가능
병목 현상: 다양한 유형의 성능저하가 발생하는 지점
CPU 바운드: CPU만 완료되면 되는 작업을 중심으로 구현되는 알고리즘
-> 알고리즘의 성능 CPU의 성능에 좌우
I/O 바운드: 입출력 장치에 의존하는 알고리즘
-> 실행 시간은 입출력 장치의 속도에 따라 달라짐
-> 네트워킹이나 컴퓨터 주변기기로부터 입력을 받는 작업들도 I/O 작업
다중 코어에서 병렬성을 활용하면 성능 향상
단일 코어에서 동시성을 구현하면 성능 저하
-> 하나의 코어에 스레드 여러개 교차 배치됨
-> 컨텍스트 스위칭(context switching) 오버헤드 발생
레이스 컨디션(Race condition): 동시성 코드 일부가 제대로 동작하기 위해 일정한 순서로 완료되어야 할때 발생
-> 정보에 접근하기 전 정보를 얻을 때까지 명시적으로 기다려야
원자성 위반(atomic operations)
-> 객체의 상태가 동시에 수정될 수 있을 때 원자성 필요
-> 객체의 상태의 수정이 겹치지 않도록 보장해야
교착 상태(deadlock): 순환적 의존성으로 인해 전체 어플리케이션의 실행이 중단되는 상황
-> 일반적으로 복잡한 잠금 관계(network of locks)에 의해 발생
-> 일반적으로 레이스 컨디션과 자주 같이 발생
라이브 락(live lock): 애플리케이션의 상태 지속적으로 변하지만 어플리케이션이 정상 실행으로 돌아오지 못하게 하는 방향으로 상태가 변하는 경우
fun main(args: Array<String>) = runBlocking{
val time = measureTimeMillis{
val name = getName()
val lastName = getLastName()
println("Hello, $name $lastName")
}
println("Execution took $time ms")
}
suspend fun getName(): String{
delay(1000)
return "Sunjoo"
}
suspend fun getLastName(): String{
delay(1000)
return "Lee"
}
Hello, Sunjoo Lee
Execution took 2010 ms
fun main(args: Array<String>) = runBlocking{
val time = measureTimeMillis{
val name = async{ getName()}
val lastName = async{ getLastName()}
println("Hello, ${name.await()} ${lastName.await()}")
}
println("Execution took $time ms")
}
Hello, Sunjoo Lee
Execution took 1022 ms
suspend fun getProfile(id: Int) : Profile{
val basicUserInfo = asyncGetUserInfo(id)
val contactInfo = asyncGetContactInfo(id)
return createProfile(basicUserInfo.await(), contactInfo.await())
}
백그라운드에서 실행될 두 메소드 호출 & 정보를 처리하기 전 완료를 기다림
suspend 메소드
-> async{} 또는 launch{} 블록 안에서 호출
동시성 코드를 쉽게 구현할 수 있는 고급함수, 기본형 제공
스레드 생성:
스레드 이름을 파라미터로 newSingleThreadContext() 호출
스레드 풀 생성:
크기와 이름을 파라미터로 newFixedThreadPoolContext() 호출
CommandPool: CPU 바운드 작업에 최적인 스레드 풀
-> 최대 크기: 시스템 코어의 수 - 1
코루틴을 다른 스레드로 이동시키는 역할은 런타임이 담당
코루틴의 통신과 동기화를 위해 필요한 많은 기본형과 기술 제공
-> ex. 채널, 뮤텍스, 스레드 한정, ...
채널(channel): 코루틴 간에 데이터를 안전하게 보내고 받는 데 사용할 수 있는 파이프
작업자 풀(Workers pools): 많은 스레드에서 연산 집합의 처리를 나눌 수 있는 코루틴의 풀
액터(Actors): 채널과 코루틴을 사용하는 상태를 감싸는 래퍼
-> 여러 스레드에서 상태를 안전하게 수정하는 매커니즘 제공
뮤텍스(Mutex): 한 번에 하나의 스레드만 실행할 수 있도록 하는 동기화 메커니즘
-> 크리티컬 존(Critical Zone) 영역 정의
스레드 한정(Thread confinement): 코루틴의 실행을 제한해서 지정된 스레드에서만 실행하도록 하는 기능
생상자(반복자 및 시퀸스): 필요에 따라 정보 생성 가능, 새로운 정보가 필요하지 않을 때 일시 중단될 수 있는 데이터 소스
일시 중단 함수: 함수 형식의 일시 중단 연산
suspend 제어자 사용
suspend fun greetAfter(name: String, delayMillis: Long){
delay(delayMillis)
println("Hello, $name")
}
delay(): 자체가 일시 중단 함수
-> 주어진 시간 실행 일시 중단
-> delay()가 완료되면 greetAfter() 실행 정상적으로 다시 시작
greetAfter()가 일시중단된 동안 실행 스레드 다른 연상을 수행하는 데 사용됨
코루틴 디스패처(Coroutine Dispatcher): 코루틴을 시작하거나 재개할 스레드를 결정하기 위해 사용
모든 코루틴 디스패처는 CoroutineDispatcher 인터페이스 구현해야
Common Pool: 공유된 백그라운드 풀에서 코루틴을 실행하고 다시 시작
-> 기본 크기: CPU 바운드 작업에서 사용하기에 적합
Unconfined: 현재 스레드에서 코루틴을 시작, 어떤 스레드에서도 코루틴이 다시 재개될 수 있음
newSingleThreadContext(): 단일 스레드로 디스패치 생성
-> 여기서 실행되는 코루틴 항상 같은 스레드에서 실행되고 재개
newFixedThreadPoolContext(): 지정된 크기의 스레드 풀이 있는 디스패처 생성
-> 디스패처에서 실행된 코루틴을 시작하고 재개할 스레드 런타임이 결정
코루틴 빌더: 일시 중단 람다를 받아 그것을 실행시키는 코루틴을 생성하는 함수
async: 결과가 예상되는 코루틴을 시작하는 데 사용
-> 코루틴 내부에서 일어나는 모든 예외를 캡처해서 결과에 넣음
-> 결과 또는 예외를 포함하는 Deferred 반환
launch: 결과를 반환하지 않는 코루틴을 시작하는데 사용
-> 자체 혹은 자식 코루틴의 실행을 취소하기 위해 사용할 수 있는 Job 반환
runBlocking: 블로킹 코드를 일시 중지 가능한 코드로 연결하기 위해 작성
-> 보통 main() 메소드와 유닛 테스트에서 사용됨
-> ⚡코루틴의 실행이 끝날 때까지 현재 스레드 차단
val result = GlobalScope.async{
isPalindrome(word = "Sample")
}
result.await()
val result = GlobalScope.async(Dispatchers.Unconfined){
isPalindrome(word = "Sample")
}
result.await()