Home coroutine 에서 원자성 위반 문제를 해결하는 방법
포스트
취소

coroutine 에서 원자성 위반 문제를 해결하는 방법

  • 시작하기 앞서 코틀린 채널에 대해서 다시 한번 떠올려 보자
  • 채널이란 : 2개의 코루틴 사이를 연결한 파이프라인 정도라고 보면 된다.
  • 스레드간 커뮤니케이션
    • 코루틴 말고도 우리는 보통 코딩을 할 때 리소스를 블로킹 하는 작업 (네트워킹, DB사용) 들은 스레드로 떼어낸다.
    • 이 스레드간 공유할 자원이 필요할때 우리는 두개의 스레드가 동시에 그걸 쓰거나 읽게 하지 못하도록 자원을 lock 하거나 메모리에 의존했다.
      • 메모리 공유 → 이것이 우리가 주로 스레드간 커뮤니케이션을 할때 사용한 방법이다.
      • 하지만 이렇게 하면 레드록, 레이스 컨디션 같은 이슈가 발생할 수 있다.
  • 채널의 커뮤니케이션은?
  • 공유 상태를 가질때 동시성 코드 블록에서 문제가 될 수 있다.
    • 스레드의 캐시, 메모리의 액세스의 원자성으로 인해 다른 스레드에서 수행한 수정 사항이 유실될 수 있다.
    • 상태의 일관성을 해치는 원인이 된다.
  • 그래서 스레드 동기화를 할 수 있는 두 가지 방법이 있다.
    • 스레드 한정
      • 공유 상태에 접근하는 모든 코루틴을 단일 스레드에서 실행되도록 한정하는 것
      • 상태가 더 이상 스레드 간에 공유되지 않으며 하나의 스레드만 상태를 수정함
      • 즉, 하나의 스레드만 상태와 상호 작용하도록 보장해서 쓰기가 아닌 읽기 전용으로만 공유할 수 있게 하는것
    • 상호 배제
      • 한 번에 하나의 코루틴만 코드 블록을 실행 할 수 있도록 하는 동기화 매커니즘
      • 코드 블록을 원자적으로 만들기 위해서 잠금을 사용해 코드 블록을 실행하려는 모든 스레드의 동기화를 강제하는 것

스레드 한정


  1. 코루틴자체를 단일 스레드로 한정

    1
    
     private val context = newSingleThreadContext("counterActor")
    
    • 앱의 여러 다른 부분에서 공유 상태를 수정해야 하거나
    • 원자 블록에 더 높은 유연성을 원하는 시나리오의 경우 이를 확장하는 방법이 필요하다.
      • 그래서 필요한게 ⇒ 액터
  2. 액터

    • 동작원리
      • 상태에 대한 접근은 단일 스레드로 한정!
      • 다른 스레드가 액터에게 message로 상태에 대한 변경 요청!
      • 액터는 channel을 사용하기 때문에 message 요청은 순차적 실행을 보장
      • ⇒ 동시성으로 인한 오류 방지
    • 액터가 위 방식보다 좋은점?
      • 상태 변경에 대한 요청을 좀 더 유연하게 할 수 있다.
        • 외부에게 특정 message로 요청을 받아 처리할 수 있기 때문에 원자 블록(액터 내부 코드블럭)이 조금 더 유연해짐
    • 액터의 내부적 동작 원리
      • 액터란 스레드 혹은 객체와는 구별되는 추상적 존재
      • 액터가 차지하는 메모리 공간은 어느 다른 스레드 혹은 액터도 접근할 수 없다.
        • 즉, 액터 내부에서 일어나는 일은 어느 누구와도 공유되지 않는다.
      • 액터는 서로간에 공유하는 자원이 절대 없고, 서로간 상태에 접근할 수 없다.
      • 오직 Message만을 이용해서 액터에게 정보를 전달해 요청하는 방식
      • 액터는 channel을 이용해 순차적 처리
    • 액터 심화
      • 액터는 코루틴의 복합체다
        • 코루틴 + 코루틴 내부로 캡슐화 된 속성값 + 통신가능한 채널
        • 속성값은 사용된 코루틴 안에서만 유효하고
        • 다른 코루틴과는 채널로 통신하는 방식

상호 배제


  • 상호 배제란 한 번에 하나의 코루틴만 코드 블록을 실행할 수 있도록 하는 동기화 매커니즘을 말한다.
  • 즉, 모든 공유되는 상태의 변경들이 절대 동시 실행되지 않도록 함
  • 코루틴 뮤텍스를 사용하면 됨
    • 코틀린 뮤텍스의 가장 중요한 특징은 블록되지 않는다는 점이다.
    • 동시에 실행되면 안되는 부분을 lock() / unlock() 으로 보호한다.
    • 자바의 넌 블로킹 synchronized 정도로 생각할 수 있겠다.
    • 다만 가장 큰 차이는 Mutex.lock() 은 일시중단 함수를 통해 컨트롤 되기 때문에 → 스레드를 블록하지 않는다는 점!
      • 일시 중단 과 차단은 다르다
        • 일시중단 - 하나의 스레드에서 여러가지 작업들을 처리한다.
        • 차단 - 해당 스레드는 블록의 작업이 다 처리될 때 까지 다른 작업을 수행할 수 없다.

그 외 원자성 위반 문제를 해결할 수 있는 다른 방법들?


  • 코루틴의 방법론으로 해결하는 건 아니지만 2가지의 방법이 있다
    1. 휘발성 변수 최신화
    2. 원자적 데이터 구조 사용

휘발성 변수


  • 휘발성 변수는 구현하려는 스레드 안전(thread-safe) 카운터와 같은 문제를 해결하지 못한다.
  • 일단 스레드 안전 문제가 발생하는 이유 2가지를 다시한번 살펴보자
    1. 다른 스레드가 읽거나 수정하는 동안 스레드의 읽기가 발생할 때
    2. 다른 스레드가 수정한 후 스레드의 읽기가 발생하지만, 스레드의 로컬 캐시가 업데이트되지 않았을 때
      • 2번은 휘발성 변수 최신화로 해결이 가능하다.
      • 1번은 … 휘발성 변수 최신화로 해결 불가
  • 휘발성 변수 최신화 하는 방법
  • @Volatile 주석을 사용할 수 있다.

    1
    2
    
      @Volatile
      var shutdownRequested = false
    
    • @Volatile은 Kotlin/JVM에서만 사용할 수 있다.
    • 휘발성(volatility)을 보장하는 기능을 JVM의 기능을 사용하기 때문에, 다른 플랫폼에서는 사용할 수 없다.
  • 그렇다면 @Volatile를 사용할 수 있는 경우?
    • 두 가지 시나리오 전제가 참이어야 한다.
      1. 변수 값의 변경은 현재 상태에 의존하지 않는다.
      2. 휘발성 변수는 다른 변수에 의존하지 않으며, 다른 변수도 휘발성 변수에 의존하지 않는다.
    • 이렇게 되면 다른 스레드가 읽거나 수정하는 동안 다른 스레드의 읽기가 발생하는 것 과 관련없어지므로 괜찮다.
    • ex) 변수값의 변경은 항상 a → b로만 변경하며 / 휘발성 변수의 값이 다른 변수의 값에 의존하지 않음

원자적 데이터 구조


  • JVM에서 제공하는 원자적 데이터 구조 사용
  • ex) AtomicInteger()…

    1
    2
    
      val counter = AtomicInteger()
      counter.incrementAndGet()
    

결론


  • 코루틴에서 동시성 코드 작성시 생기는 문제 중 → 원자성 위반의 문제를 해결하기 위해 도대체 어떤 방법을 사용해야할까?
  • 상호배제를 사용해야하는 경우
    • 어떠한 공유되는 상태를 어쩔 수 없이 주기적으로 변경해야하며 → 이를 위해 스레드 한정을 사용할 수 없을 때
  • 스레드 한정을 사용해야하는 경우
    • 다른 컨텍스트로 전환해야 하는 비용이 없기 때문에 부하 측면에서는 locking을 하는 경우보다 효율적이다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

gRPC 서버에서 ThreadLocal을 올바르게 사용하는 방법 (gRPC Context, gRPC + JPA AuditorAware)

코틀린 - 변성

Comments powered by Disqus.