관리 메뉴

나만을 위한 블로그

[Kotlin] 세마포어, 뮤텍스 구현 예제 본문

개인 공부/Kotlin

[Kotlin] 세마포어, 뮤텍스 구현 예제

참깨빵위에참깨빵 2023. 4. 4. 00:21
728x90
반응형

https://onlyfor-me-blog.tistory.com/720

 

세마포어(Semaphore)란? 뮤텍스(Mutex)란? 교착 상태(deadlock)란?

안드로이드 개발을 하면서 동시성 프로그래밍을 한다면 코루틴을 주로 사용하기 때문에 제목의 2가지 개념은 직접 사용할 일이 없지만, 알아둬서 나쁜 개발 지식은 없다고 생각하기 때문에 포

onlyfor-me-blog.tistory.com

 

이전 포스팅에서 세마포어, 뮤텍스가 각각 무엇인지 확인했으니 코틀린을 써서 어떻게 구현할 수 있는지 간단하게 확인해본다.

 

먼저 세마포어다. 세마포어는 "java.util.concurrent.Semaphore"를 써서 구현할 수 있다.

 

import kotlinx.coroutines.*
import java.util.concurrent.Semaphore

suspend fun main() {
    val numCoroutines = 10
    val maxConcurrentCoroutines = 3
    val semaphore = Semaphore(maxConcurrentCoroutines)

    val jobs = mutableListOf<Job>()

    repeat(coroutineCount) { i ->
        val job = CoroutineScope(Dispatchers.IO).launch {
            withContext(Dispatchers.IO) {
                semaphore.acquire()
            }

            try {
                synchronized(System.out) {
                    println("Coroutine $i started")
                }
                delay(1000)
                synchronized(System.out) {
                    println("Coroutine $i finished")
                }
            } finally {
                semaphore.release()
            }
        }
        jobs += job
    }

    jobs.forEach { it.join() }
}

// Coroutine 0 started
// Coroutine 3 started
// Coroutine 4 started
// Coroutine 3 finished
// Coroutine 0 finished
// Coroutine 9 started
// Coroutine 4 finished
// Coroutine 8 started
// Coroutine 1 started
// Coroutine 9 finished
// Coroutine 8 finished
// Coroutine 6 started
// Coroutine 1 finished
// Coroutine 5 started
// Coroutine 2 started
// Coroutine 5 finished
// Coroutine 2 finished
// Coroutine 6 finished
// Coroutine 7 started
// Coroutine 7 finished

 

처음에 최대 동시 실행 가능한 코루틴 수를 3으로 설정한 걸 볼 수 있다. 이것을 기억해 두고 코드를 확인한다.

코루틴을 써야 하기 때문에 main 함수를 suspend function으로 지정한 다음, repeat()을 통해 10개의 코루틴을 생성하고, semaphore 객체를 통해 acquire()를 호출해 코루틴이 잠금의 소유권을 갖는다. 콘솔 출력을 예시로 들면 Coroutine 0, 3, 4가 맨 처음 잠금을 갖게 되는 것이다. 왜 3개냐면 앞에서 최대 동시 실행 가능한 코루틴 수를 3으로 지정했기 때문이다.

그 다음 3줄을 보면 처음에 잠금을 획득했던 Coroutine 3, 0이 종료되고 9가 시작된다. 4는 아직 종료되지 않았다.

그 다음 3줄을 보면 4가 끝나고 8, 1이 시작된다. 9는 아직 종료되지 않았다.

이런 식으로 시작된 코루틴이 작업을 마치면 finished를 출력하면서 잠금의 소유권도 같이 포기하게 된다. 이 때 기존에 생성되서 대기하고 있던 코루틴 중 하나가 잠금을 얻어서 started를 출력하는 것이다.

println()을 synchronized 블록에 넣은 이유는 println()이 버퍼링되는 특징이 있어서 여러 줄의 출력이 순차적으로 출력되지 않고 한 번에 출력되는 경우가 발생할 수 있기 때문이다. 그래서 synchronized 블록으로 감싸지 않으면 시작되지도 않았던 코루틴이 종료되던가 하는 콘솔 출력이 나타난다.

 

정리하면 이 코드가 실행될 경우 코루틴 스케줄러에 의해 선택된 코루틴 3개가 acquire()를 통해 잠금을 획득한다. 그리고 동시 실행 가능한 코루틴의 개수가 3으로 지정돼 있기 때문에, 3개 이상의 코루틴이 동시에 실행될 일은 없다. 잠금을 획득한 코루틴은 작업을 진행하고(여기선 delay()만 호출), 종료되면 release()를 통해 갖고 있던 잠금을 해제하고 finished를 출력한 다음 종료된다. 그러면 다시 코루틴 스케줄러가 선택한 코루틴이 잠금을 얻게 되어 기존 코루틴이 실행했던 작업들을 수행하고 finished를 출력하며 종료된다.

주의할 것은 코루틴이 무작위로 뽑힌다고는 하지 않았다. 코루틴이 무작위로 뽑힌다는 표현은 정확하지 않고, 내부적으로 코루틴 스케줄러와 OS 스케줄러, 기타 다양한 시스템적 요소가 얽혀서 어떤 코루틴이 먼저 실행될지가 결정되기 때문이다.

 

다음은 뮤텍스 구현 예제다. 코틀린에서 뮤텍스는 "kotlinx.coroutines.sync.Mutex"를 써서 구현할 수 있다.

 

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

suspend fun main() {
    val mutex = Mutex()
    val sharedResource = SharedResource()

    val jobs = List(10) { index ->
        CoroutineScope(Dispatchers.IO).launch {
            delay((index * 100).toLong())
            mutex.withLock {
                sharedResource.increment()
                println("Coroutine $index incremented value: ${sharedResource.value}")
            }
        }
    }

    jobs.forEach { it.join() }
}

class SharedResource {
    var value = 0

    fun increment() {
        value++
    }
}

// Coroutine 0 incremented value: 1
// Coroutine 1 incremented value: 2
// Coroutine 2 incremented value: 3
// Coroutine 3 incremented value: 4
// Coroutine 4 incremented value: 5
// Coroutine 5 incremented value: 6
// Coroutine 6 incremented value: 7
// Coroutine 7 incremented value: 8
// Coroutine 8 incremented value: 9
// Coroutine 9 incremented value: 10

 

이 예제에선 코루틴이 순차적으로 공유 자원에 접근해서 value 값을 1씩 증가시킨다. 뮤텍스를 사용하지 않았다면 어떻게 될까?

위 코드에서 2개의 코루틴이 동시에 value 값을 읽고 1을 증가시킨 뒤 저장한다고 가정한다. 그러면 코루틴 2개가 동시에 같은 값을 읽고 쓰기 때문에 공유되는 리소스(value) 값이 2번 증가되어야 하겠지만 실제로는 1번만 증가한다. 즉 경쟁 조건으로 인해 공유 리소스의 값이 이상하게 바뀌는 문제가 발생할 수 있다. 이 때 뮤텍스를 쓰면 공유 리소스에 대한 접근을 동기화해서 한 번에 하나의 코루틴만 해당 리소스에 접근할 수 있게 해준다. 그래서 뮤텍스를 쓰면 경쟁 조건이 발생하지 않고 내 예상대로 값이 증가하게 되는 것이다.

 

아래는 다른 뮤텍스 예제다. 코루틴 2개가 같은 변수에 접근해서 count를 증가시키며 출력하고 19가 되면 종료된다.

 

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

suspend fun main() {
    val mutex = Mutex()
    var count = 0
    val maxCount = 20

    val coroutine1 = CoroutineScope(Dispatchers.IO).launch {
        while (count < maxCount) {
            mutex.withLock {
                if (count < maxCount) {
                    println("Coroutine1: ${count++}")
                }
            }
            delay(1000)
        }
    }

    val coroutine2 = CoroutineScope(Dispatchers.IO).launch {
        while (count < maxCount) {
            mutex.withLock {
                if (count < maxCount) {
                    println("Coroutine2: ${count++}")
                }
            }
            delay(1000)
        }
    }

    coroutine1.join()
    coroutine2.join()
}

// Coroutine2: 0
// Coroutine1: 1
// Coroutine2: 2
// Coroutine1: 3
// Coroutine2: 4
// Coroutine1: 5
// Coroutine2: 6
// Coroutine1: 7
// Coroutine2: 8
// Coroutine1: 9
// Coroutine2: 10
// Coroutine1: 11
// Coroutine2: 12
// Coroutine1: 13
// Coroutine2: 14
// Coroutine1: 15
// Coroutine1: 16
// Coroutine2: 17
// Coroutine1: 18
// Coroutine2: 19

 

코루틴1, 2 중 어떤 코루틴이 먼저 값을 증가시킬지는 정해져 있지 않기 때문에 실행할 때마다 랜덤한 코루틴이 먼저 일을 시작하지만 둘이 번갈아가며 값을 1씩 증가시키는 건 변하지 않는다.

반응형
Comments