ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 13-4. 동시성 : 자바 동시성 사용하기
    Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 12. 11. 17:06

    13장 - 동시성

     

    13-1. 코루틴

    13-2.  코루틴 흐름 제어와 잡 생명 주기

    13-3.  동시성 통신

    13-4.  자바 동시성 사용하기


    13-4.  자바 동시성 사용하기

     

    JVM 플랫폼에서는 JDK가 제공하는 동기화 요소를 활용할 수 있다.

    코틀린 표준 라이브러리는 스레드 생성 / 동기화 등 관련 작업을 편하게 도와준다.

    1) 스레드 시작하기

    범용 스레드를 시작하려면 스레드에서 실행하려는 Runnable 객체에 대응하는

    람다와 스레드 프로퍼티들을 지정해서 thread() 함수를 사용하면 된다.

     

     ● start : 스레드를 생성하자마자 실행할지 여부(dafault: true)

     ● isDaemon : 스레드를 데몬 모드로 시작할지 여부(dafault: false)

     데몬 모드는 JVM의 종료를 방해하지 않고, 메인 스레드가 종료될 때 자동으로 함께 종료

     ● contextClassLoader : 스레드 코드가 클래스와 자원을 적재할 때 사용할 클래스 로더(dafault: null)

     ● name : 커스텀 스레드 이름(dafault: null = JVM이 이름을 자동으로 지정)

     ● priority: 어떤 스레드가 다른 스레드에 비해 얼마나 많은 CPU 시간을 배정받는지 결정

      Thread.MIN_PRIORITY(=1) ~ Thread.MAX_PRIORITY(=10) 사이의 값

      Default : -1, 자동으로 우선순위를 정하라는 의미

     ● block: () -> Unit 타입의 함숫값, 새 스레드가 생성되면 실행할 코드

     

    아래 코드는 150ms마다 메시지를 출력하는 스레드이다

    fun main() {
        println("Starting a thread..")
    
        thread(name = "Worker", isDaemon = true) {
            for (i in 1..5) {
                println("${Thread.currentThread().name}: $i")
                Thread.sleep(150)
            }
        }
        Thread.sleep(500)
        println("Shutting down..")
    }
    //Starting a thread..
    //Worker: 1
    //Worker: 2
    //Worker: 3
    //Worker: 4
    //Shutting down..

    isDaemont = true로 설정했으므로 메인 스레드가 500ms 이후에 실행을 끝낼 때

    스레드도 함께 끝나므로 메시지가 4개만 출력된다.


    어떤 지정한 시간 간격으로 동작을 수행하는 자바 타이머 관련 함수가 있다.

    timer() 함수는 어떤 작업을 이전 작업이 끝난 시점을 기준으로

    고정된 시간 간격으로 실행하는 타이머를 설정한다.

    그 결과 어떤 작업이 시간이 오래 걸리면 이후의 모든 실행이 연기된다.

    따라서 이 타이머는 앞에서 봤던 FIXED_RATE 모드로 작동하는 코틀린 티커에 비유할 수 있다.

    추가로 두 타이머 이벤트 사이의 시간 간격을 최대한 일정하게 맞춰주는 fixedRateTimer()함수도 있는데,

    이는 앞에서 봤던 FIXED_PERIOD 모드의 티커에 비유할 수 있다.

     

    아래 코드는 앞의 thread를 이용한 코드를 타이머를 이용해 작성한 것 이다

    fun main() {
        println("Starting a thread..")
        var counter = 0
    
        timer(period = 150, name = "Worker", daemon = true) {
            println("${Thread.currentThread().name}: ${++counter}")
        }
    
        Thread.sleep(500)
        println("Shutting down..")
    }

    thread를 이용한 코드와 결과는 동일한 것을 확인할 수 있다.


    2) 동기화와 락

    동기화는 특정 코드 조각이 한 스레드에서만 실행되도록 보장하기 위한 공통적인 기본 요소다.

    이런 코드 조각을 다른 스레드가 실행하고 있다면

    해당 코드에 진입하려고 시도하는 다른 스레드들은 모두 대기해야 한다.

    자바에서는 코드에 동기화를 도입하는 두 가지 방법이 있다.

    1. 락으로 사용하려는 어떤 객체를 지정하는 특별한 동기화 블록을 사용해 동기화하는 코드를 감싸는 방법

    2. 메서드에 synchronized 변경자를 붙이는 것

     

    첫 번째 방법을 먼저 살펴보면, 자바와 코틀린의 동기화 구분은 상당히 비슷하다.

    대신 자바와 달리 언어에 내장된 구조를 사용하는게 아닌

    표준 라이브러리 함수를 사용한다는 차이점이 있다.

    fun main() {
        var counter = 0
        val lock = Any()
    
        for (i in 1..5) {
            thread(isDaemon = false) {
                synchronized(lock) {
                    counter += i
                    print(counter)
                    print(' ')
                }
            }
        }
    }
    //1 6 10 12 15

    개별 덧셈의 결과는 달라질 수 있어서 중간 결과가 바뀔 수 있지만,

    동기화로 인해 합계는 항상 15가 출력된다.

     

    일반적으로 synchronized() 함수는 람다의 반환값을 반환한다.

    그래서 위 코드에서 호출되는 시점의 중간 카운터 값을 읽을 때 synchronized()를 사용할 수 있다.

    val currentCounter = synchronized(lock) { counter }
    println("Current counter: $currentCounter")

    이 코드를 통해 1 ~ 5까지 덧셈을 했던 다섯 개의 덧셈 스레드 중 하나가 만들어낸 값을 알 수 있다.


    자바에서 동기화에 사용하는 두 번째 방법은 메서드에 synchronized 변경자를 붙이는 것 이다.

    이 경우 메서드 본문 전체가 클래스 인스턴스나 클래스 인스턴스 자체에 의해 동기화된다.

    코틀린에서는 @Synchronized 어노테이션을 통해 동일한 기능을 사용할 수 있다.

    class Counter {
        private var value = 0
        @Synchronized fun addAndPrint(value: Int) {
            this.value += value
            println(this.value)
        }
    }
    fun main() {
        val counter = Counter()
        for (i in 1..5) {
            thread(isDaemon = false) { counter.addAndPrint(i) }
        }
    }

    동기화 블록과 비슷한 Lock 객체(java.util.concurrent.locks package)를 사용해

    주어진 람다를 실행하게 해주는 withLock() 함수도 있다.

    withLock을 사용하면 함수가 알아서 락을 풀어주므로,

    예외가 발생할 때 락을 푸는 것을 신경쓰지 않아도 된다.

    class Counter {
        private var value = 0
        private val lock = ReentrantLock()
        fun addAndPrint(value: Int) {
            lock.withLock {
                this.value += value
                println(this.value)
            }
        }
    }
    fun main() {
        val counter = Counter()
        for (i in 1..5) {
            thread(isDaemon = false) { counter.addAndPrint(i) }
        }
    }

     

    중요하다는 말을 너무나도 많이 들어서 엄청 집중하면서 작성했던 것 같다.

    나름 중간중간마다 정리를 하기도 했고 머리에 최대한 넣으려고 노력했다.

    그래도 그 덕분에 코틀린에서 동시성을 다룰 때 어떻게 하는지 알게 된 것 같다.

     

    다음 단원은 이 책 스터디의 마지막장이다. 코틀린 테스팅에 대한 내용이다.

    테스트..도 중요하다는 말을 많이 듣긴 했다. 최근 들어 TDD 얘기도 많이 나오고 있고

    마지막까지 힘내서 작성해봐야겠다.

    반응형

    댓글

Designed by Tistory.