동시성과 병렬성

: ) YOUNG·2023년 12월 8일
1

Kotlin

목록 보기
7/7
post-thumbnail
  • 올바른 동시성 코드는 항상 같은 결과를 내놓지만, 실행 순서에는 약간의 가변성을 허용하는 코드이다.
    그러려면, 코드의 서로 다른 부분이 어느 정도 독립성이 있어야 하며 약간의 조정도 필요하다. 동시성을 이해하는 가장 좋은 방법은 순차적인 코드를 동시성 코드와 비교하는 것이다.


fun getProfile(id : Int) : Profile {
	val basicUserInfo = getUserInfo(id)
    val contactInfo = getContactInfo(id)

	return createProfile(basicUserInfo, contactInfo)
} // End of getProfile()

순차 코드의 문제

  • 동시성 코드에 비해 성능이 저하될 수 있다.

  • 코드가 실행되는 하드웨어를 제대로 활용하지 못할 수 있음

위 코드 에서 getUserInfo(), getContactInfo()가 각 1초가 소요된다고 하면 getProfile()는 2초 이상의 시간이 필요하다.

여기서 getUserInfo(), getContactInfo()를 동시에 호출하면 절반의 시간으로 줄일 수 있다.


suspend fun getProfile(id : Int) {
	val basicUserInfo = asyncGetUserInfo(id)
    val contactInfo = asyncGetContactInfo(id)
    
    createProfile(basicUserInfo.await(), contactInfo.await())
} // End of getProfile()

해당 코드에서는 asyncGetUserInfo()asyncGetContactInfo() 보다 먼저 수행이 완료될지 알 수 없다.

실제로 이전의 순차적인 버전보다 2배 빠르게 수행될 수 있지만 실행할 때 가변성이 발생한다.

그래서 createProfile() 내부에 await()호출이 있는 이유이다.
이것이 하는 일은 asyncGetUserInfo()asyncGetContactInfo()가 모두 완료될 때 까지 getProfile()의 실행을 일시 중단하는 것이다.

둘 다 완료됐을 때만 createProfile()이 실행된다. 어떤 동시성 호출이 먼저 종료되든지에 관계없이 getProfile()의 결과가 항상 같은 결과가 나옴을 보장한다.


요약하자면

동시성은 2개 이상의 실행 시간이 겹칠 때 발생한다.

중첩이 발생하려면 2개 이상의 실행 쓰레드가 필요하다. 이런 쓰레드 들이 단일 코어에서 실행되면 병렬이 아니라 동시에 실행되는데, 단일 코어가 서로 다른 쓰레드의 인스트럭션으 교차 배치해서, 쓰레드들의 실행을 효율적으로 겹쳐서 실행한다.

병렬은 2개의 알고리즘이 정확히 같은 시점에 실행될 때 발생한다.

이것이 가능하려면 2개 이상의 코어와 2개 이상의 쓰레드가 있어야 각 코어가 동시에 쓰레드의 인스트럭션을 실행할 수 있다. 병렬은 동시성을 의미하지만 동시성은 병렬성이 없이도 발생할 수 있다.



원자성 위반

원자성 작업이란 작업이 사용하는 데이터를 간섭없이 접근할 수 있음을 의미한다.

단일 쓰레드에서는 모든 코드가 순차적으로 동작하기 때문에 모든 작업이 모두 원자일 것이다. 쓰레드가 하나만 있기 때문에 간섭이 있을 수 없다.

수정이 겹칠 수 있다는 것은 데이터의 손실이 발생할 수 있다는 뜻인데,
코루틴이 다른 코루틴이 수정하고 있는 데이터를 바꿀 수 있다는 것이다.


var count = 0;

fun main() = runBlocking {
    val workerA = asyncIncrement(2000)
    val workerB = asyncIncrement(100)
    workerA.await()
    workerB.await()

    println("count : ${count}")
} // End of main

private fun asyncIncrement(by : Int) = GlobalScope.async{
    for(i in 0 until by) {
        count++
    }
} // End of asyncIncrement


실제로 위 코드는 asyncIncrement() 코루틴을 2번 동시에 실행한다.
문제는 여기서 두 실행이 서로 간섭이 일어날 수 있으며, 서로 다른 코루틴 인스턴스가 값을 재정의 할 수 있다는 것이다. main()을 실행하면 대부분 "count : 2100"을 출력하지만, 꽤 많은 출력에서 2,100보다 적은 출력값을 볼 수 있다.

코루틴에서 명령이 중첩되는 것은 count++ 작업이 원자적이지 않기 때문이다.

count++의 원자성이 없기 때문에 두 코루틴이 다른 코루틴이 하는 조작을 무시하고 값을 읽고 수정할 수 있다.

0개의 댓글