동시성 프로그래밍(Concurrent Programming)

동시성 프로그래밍은 여러개의 계산들을 연속적으로 하는 것이 아닌 병행 처리하는 프로그램을 말한다. 이러한 동시성 코드를 제대로 작성하는 것은 쉽지 않다. 하지만 동시성 코드 작성을 통해 성능 향상을 기대할 수 있는 경우가 많기 때문에 현대 프로그래밍에서는 필수적인 요소다.

동시성 코드를 작성할 때 발생할 수 있는 공통된 문제점들에 대해서 알아보도록한다.

경쟁 상태(Race Condition)

경쟁 상태란 둘 이상의 입력 또는 조작의 타이밍이나 순서 등이 결과값에 영향을 줄 수 있는 상태를 말한다. 입력 변화의 타이밍이나 순서가 예상과 다르게 작동하면 정상적인 결과가 나오지 않게 될 위험이 있는데 이를 경쟁 위험이라고 한다.

다음의 예를 통해 경쟁상태를 이해해보자.

 

아들은 은행에 10000원의 잔고가 있었고, 현금 인출기를 통해 잔고 10000원을 출금하고 있다. 그 사이 엄마는 아들에게 용돈을 5000원 입금 해주었다. 그렇다면 잔고는 얼마일까?
아들은 10000원을 인출했기 때문에 잔고는 0원이 되고, 이후 엄마가 5000원을 입금해주신 덕분에 잔고는 5000원이 되리라 기대할 것이다. 

우리가 이렇게 생각할 수 있는 이유는 아들이 먼저 인출을 하고 엄마가 그 다음으로 입금했으리라 가정했기 때문이다.

만약 아들과 엄마가 동시에 출금 및 입금을 시도했다면 어떻게 될까?

동시에 출금과 입금이 이루어지는 경우
아들의 입장 : 현재잔고 10000원 – 10000원 출금 = 기대잔고 0원
엄마의 입장 : 현재잔고 10000원 – 5000원 입금 = 기대잔고 15000원

이처럼 경쟁상태는 공유 데이터(잔고)에 최종값을 보장할 수 없는 상황을 말한다. 최악의 상황에는 엄마가 5천원을 입금해주었으나 잔고가 0원이 될 수도 있고, 아들은 10000원을 출금하였으나 잔고는 15000원인 상황이 될 수도 있는 것이다.

이런 문제를 해결하기 위해 임계구역을 지정하고 동기화 메커니즘을 사용한다.

임계 구역(Critical Section)

임계 구역(critical section) 또는 공유변수 영역은 병렬컴퓨팅에서 둘 이상의 스레드가 동시에 접근해서는 안되는 공유 자원(자료 구조 또는 장치)을 접근하는 코드의 일부를 말한다. 임계 구역은 지정된 시간이 지난 후 종료된다. 때문에 어떤 스레드(태스크 또는 프로세스)가 임계 구역에 들어가고자 한다면 지정된 시간만큼 대기해야 한다. 스레드가 공유자원의 배타적인 사용을 보장받기 위해서 임계 구역에 들어가거나 나올때는 세마포어 같은 동기화 매커니즘이 사용된다.

위의 은행을 예로 들면 은행의 잔고는 임계구역으로 지정하여 둘 이상의 스레드(엄마와 아들)가 동시에 접근하지 못하게 만들어야 한다.

세마포어(Semaphore)

세마포어(Semaphore)는 에츠허르 데이크스트라가 고안한, 두 개의 원자적 함수로 조작되는 정수 변수로서, 멀티 프로그래밍 환경에서 공유 자원에 대한 접근을 제한하는 방법으로 사용된다. 이는 철학자들의 만찬 문제의 고전적인 해법이지만 모든 교착상태를 해결하지는 못한다.

다음 예제코드를 살펴보자

@Test
fun count() {
    val count = 10000
    var value = 0
    val thread1 = Thread {
        repeat(count) {
            value++
        }
    }
    val thread2 = Thread {
        repeat(count) {
            value--
        }
    }
    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()
    println("value = $value")
}

단순히 두개의 스레드를 사용하여 각각 value를 10000번 증감시키는 예제이다. 각각 10000번씩 증가 및 감소 하기 때문에 value가 0이 나오길 기대하지만, 경쟁상태가 빈번히 발생하기 때문에 실행할 때마다 결과 값이 다르다.

실행결과
value = 1916

이제 세마포어를 사용하여 이 코드를 개선해보자.

@Test
fun count() {
    val semaphore = Semaphore(1)
    val count = 10000
    var value = 0
    val thread1 = Thread {
        repeat(count) {
            semaphore.acquire()
            value++
            semaphore.release()
        }
    }
    val thread2 = Thread {
        repeat(count) {
            semaphore.acquire()
            value--
            semaphore.release()
        }
    }
    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()
    println("value = $value")
}

세마포어의 acquire() 호출을 통해 다른 스레드의 진입을 막고, 작업이 끝나면 release()를 호출하여 다른 스레드의 진입을 허용한다.

실행결과
value = 0

교착 상태(Deadlock) 

교착상태란 두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태를 가리킨다. 

흔히 철학자들의 만찬을 예제로 교착상태를 설명하는데 이를 조금 수정해보도록 하겠다.

젓가락 한쌍이 있고, 식탁위에 음식 그리고 두 사람이 있다고 가정하자. 두 사람은 반드시 젓가락 한쌍으로만 음식을 먹을 수 있다. 이 때 두 사람에게 각각 젓가락 1개씩 주어진다. 두 사람이 번갈아가면 젓가락을 사용하면 음식을 먹을 수 있겠지만, 두 사람 모두 먼저 음식을 먹기를 원하기 때문에 상대방에게 젓가락을 양보하지 않고 기다리는 상황이 생길 수 있다. 이를 교착상태라고 한다. 

교착상태가 발생하는 예제코드를 살펴보자

lateinit var job1:Thread
lateinit var job2:Thread
@Test
fun deadlock(){
    job1= Thread{
        Thread.sleep(1000)
        //job2가 끝나길 기다림
        job2.join()
    }
    job2 = Thread{
        //job1이 끝나길 기다림
        job1.join()
    }
    job1.start()
    job2.start()
    //job1이 끝나길 기다림
    job1.join()
    println("모든 작업 완료")
}

job1과 job2가 서로의 작업이 끝나길 기다리기 때문에 교착상태가 발생하기 때문에, “모든 작업 완료”라는 메시지의 출력을 확인할 수 없다.

 

 

 

카테고리: etc

1개의 댓글

성빈 · 2023년 6월 27일 5:02 오후

깔끔한 정리 감사합니다!

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.