아래 교재 및 문서를 보고 공부한 내용을 정리한 공간입니다.
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: 경합 조건
-
동시성 코드를 작성할 때 가장 흔한 오류
-
코드를 동시성으로 작성했지만 순차적 코드처럼 동작할 것이라고 예상할 때 발생
-
동시성 코드가 항상 특정한 순서로 실행될 것이라 가정하고 오해할 때 생기는 문제
-
정보에 접근하려고 하기 전에 정보를 얻을 때까지 명시적으로 기다리도록 해야 함
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")
}
-
순차적으로 실행될 것이라고 예상하고
user
에 접근. 명시적으로asyncGetUserInfo
함수를 기다도록 해야 함
원자성 위반atomic operations
-
작업이 사용하는 데이터를 간섭 없이 접근할 수 있음을 말함
-
단일 스레드 어플리케이션에서는 모든 코드가 순차적으로 실행되기 때문에 모든 작업이 모두 원자일 것
-
원자성은 객체의 상태가 동시에 수정될 수 있을 때 필요, 상태의 수정이 겹치지 않도록 보장해야 함
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