Kotlin

Kotlin 동시성 프로그래밍

dev_roach 2022. 11. 7. 23:24
728x90

동기식 프로그래밍

초기 컴퓨터의 문제

초기 컴퓨터는 하나의 프로그램만을 실행시키는 역할을 했다. 그래서 초기 시분할 시스템에서는 각 프로세스가 가상적인 폰 노이만 컴퓨터 였다. 폰 노이만 컴퓨터 답게 각각 명령어와 데이터를 저장하는 메모리 공간을 가지고 기계어로 된 명령어를 순차적으로 수행하며, 운영체제가 제공하는 I/O 수단을 통해 외부와 교류했다.

위와 같은 이유로 초기 컴퓨터의 문제점은 다양했다. 하나의 문제를 뽑자면, 외부 입출력과 같은 작업이 지속되면 프로그램이 블락(Block)되어 시간적으로 비효율적일 수 밖에 없었다.

스레드의 등장

위와 같은 문제점을 해결하기 위한 방법론 중 하나로 스레드 가 등장하게 되었다. 스레드는 PC(프로그램 카운터), 스택, 지역 변수등을 가지고, 공유해야 할 변수는 프로세스 내 힙(Heap) 지역에 할당하여 이용한다. 이로 인해 하나의 프로그램 내에서 여러 스레드를 실행시켜 작업을 좀 더 효율적으로 할 수 있게 되었다. 초기에는 스레드를 경량 프로세스(lightweight process) 라고 불렀다. 지금의 코루틴이 경량 스레드(lightweight thread) 라고 불리는 이유와 비슷하다.

스레드의 등장으로 인해 기존에는 프로세스 내에서의 흐름이 하나뿐이였다면 이제 다수의 흐름(Flow) 로 바뀌게 되었다.

동시성 프로그래밍에서의 문제

class A {
    var a: Int = 0

    fun plus() {
        a++
    }
}

fun main() {
    val aobj = A()
    thread(start = true, isDaemon = true) {
        var i = 0
        while (i < 100000) {
            aobj.plus()
            i++
        }
    }
    thread(start = true, isDaemon = true) {
        var i = 0
        while (i < 100000) {
            aobj.plus()
            i++
        }
    }

    Thread.sleep(1000)

    println("${aobj.a}")
}

위 코드를 멀티 스레드 환경에서 실행했을때의 문제점은 무엇일까? 동시성 프로그래밍을 해봤다면 알겠지만 ++ 연산은 원자적 연산이 아니다. 즉, 값의 ++ 연산 도중에 다른 스레드에서 값을 읽게 되면 읽어온 값은 +1 이 더해진 1 이 아니라 0 으로 나오게 된다.

이러한 문제는 하나의 프로그램을 여러 흐름이 실행시키는 문제로 인해 발생한다. 즉, 멀티-스레드 프로그래밍을 하게 되면 자연스럽게 발생하는 문제이다. 이로 인해 멀티-스레드 프로그래밍을 진행할때 공유되는 변수에 관한 적절한 처리가 필요하다.

Synchronized

class A {
 var a: Int = 0

 @Synchronized fun plus() {
        a++
 }
}

동시성 문제가 발생하게 되면 여러 해결방법이 있지만 간단하게는 @synchronized 를 붙이는 방법이 있다.

class A {
    var a: Int = 0

    @Synchronized fun plus() {
        a++
    }
}

fun main() {
    val aobj = A()
    thread(start = true, isDaemon = true) {
        var i = 0
        while (i < 100000) {
            aobj.plus()
            i++
        }
    }
    thread(start = true, isDaemon = true) {
        var i = 0
        while (i < 100000) {
            aobj.plus()
            i++
        }
    }

    Thread.sleep(1000)

    println("${aobj.a}")
}

위와 같이 코드를 돌린뒤 실행해보면 순서대로 잘 처리되는것을 확인할 수 있다. Sychronized 처리를 한 함수는 암묵적인 락(intrinsic lock) 을 획득한 것과 같으며 코드로 보자면 아래와 같다.

class A {
    var a: Int = 0

    fun plus() {
        synchronized(this) {
            a++
        }
    }
}

fun main() {
    val aobj = A()
    thread(start = true, isDaemon = true) {
        var i = 0
        while (i < 100000) {
            aobj.plus()
            i++
        }
    }
    thread(start = true, isDaemon = true) {
        var i = 0
        while (i < 100000) {
            aobj.plus()
            i++
        }
    }

    Thread.sleep(1000)

    println("${aobj.a}")
}

객체 자체를 Lock 객체로써 활용하는 것이다. Java 자체에서 Mutex 로 이용되는 것이며 하나의 스레드만 Lock 객체를 소유할 수 있다.

재진입성(reentrant)

재진입성은 단어자체만 보면 어렵지만 이야기하자면, 스레드 단위로 락을 다시획득할 수 있게 해주는 것이다. 아래 예시 코드를 한번 보자.

open class A {
    var a: Int = 0

    @Synchronized fun plus() {
        a++
    }
}

class B: A() {
    @Synchronized fun justPlus() {
        super.plus()
    }
}

위와 같은 코드가 있을때 B 객체를 만든뒤 justPlus() 를 실행하게 되면 첫번째로 A 에 대한 Lock 을 취득한다. 그 뒤 super.plus() 를 호출할때 또 Lock 을 취득하려고 하지만 Lock 은 해제되지 않은 상태로 교착상태(DeadLock) 에 빠진다.

위와 같은 상황을 방지하기 위해 똑같은 스레드가 모니터 락을 이미 지니고 있을때라면 그대로 재진입(re-entry) 할 수 있도록 해주는 것 이다.

Volatile

보통 스레드가 연산을 시작하는 경우 성능 최적화를 위해 레지스터 내/외부에 값을 캐싱한다. 보통 레지스터의 경우 각 코어에서만 공유하므로 스레드마다 캐싱된 값이 다를 수 있다. Volatile 을 우리가 붙여주게 되면 JVM 은 이 변수를 캐싱하지 않아야 한다고 이해하기 때문에 항상 최신의 상태의 값을 읽을 수 있다.

다만 Volatile 은 최신의 값을 읽어온다는 “메모리 가시성” 측면에서 의미가 있는것이지 Synchronized 와 동일하게 생각해서는 안된다. 예를 들면, 위의 코드를 Volatile 을 쓴다고 해도 ThreadSafe 하지 않다.

open class A {
    @Volatile
    var b: Int = 0

    fun plus() {
        b++
    }
}

fun main() {
    val aobj = A()
    thread(start = true, isDaemon = true) {
        var i = 0
        while (i < 100000) {
            aobj.plus()
            i++
        }
        println("Done1!")
    }
    thread(start = true, isDaemon = true) {
        var i = 0
        while (i < 100000) {
            aobj.plus()
            i++
        }
        println("Done2!")
    }

    Thread.sleep(1000)

    println("${aobj.b}")
}

보통 그래서 volatile 과 같은 코드는 임계 구역에서 탈출하기 위해 플래그(Flag) 값들을 다른 스레드에서 바꿀때 해당 플래그 값이 잘 바뀌었는지 확인할 때 많이 사용된다.

참조

http://www.yes24.com/Product/Goods/3015162

 

자바 병렬 프로그래밍 - YES24

스레드는 자바 플랫폼에서 가장 기본적으로 제공되는 기능 중 하나다. 멀티코어 프로세서가 대중화되면서 고성능 애플리케이션을 작성할 때 병렬 처리 능력을 효과적으로 활용하는 일의 중요

www.yes24.com