개발놀이터

동시성문제와 Thread-safe 본문

CS 지식/운영체제

동시성문제와 Thread-safe

마늘냄새폴폴 2023. 4. 13. 04:36

동시성 문제와 Thread-safe를 고민하는 것은 개발자로서 정말 중요한 과제라고 생각됩니다. 서비스가 커지면 커질수록 멀티스레딩이 절실히 필요해질텐데 성능을 위해 멀티스레딩을 강요받았지만 동시성문제로 버그가 생겨버리면 참 골치아플 것 같습니다. 

 

이번 시간에는 동시성문제와 Thread-safe 그리고 자바에선 어떻게하면 동시성문제를 해결할 수 있는지까지 알아봅니다. 

 

동시성문제

동시성문제가 발생하는 코드는 한눈에 봐서는 알아차리기 쉽지 않습니다. 차라리 동시성문제가 발생하는 패턴을 외워두고 있다가 해당 패턴이 나오면 해결하는 것이 더 타당해보일 정도로 정말 알기 쉽지않습니다. 

 

저는 프로젝트를 할 때 동시성문제에 대해 진지하게 고민하고 코드를 짜본 적은 없지만 어떤 상황이 동시성 문제가 발생하는지는 대강 알고있습니다. 

 

해당 챕터에서는 어떤 상황이 동시성문제가 발생할 수 있는지 타임라인을 따라가보며 자세히 알아보도록 하겠습니다. 

 

public class BasicObservableClass {
   public interface Observer {
     void onObservableChanged();
   }
   private Set<Observer> mObservers;
   public void registerObserver(Observer observer) {
     if (observer == null) {
       return;
     }
     if (mObservers == null) {
       mObservers = new HashSet<>(1);
     }
     mObservers.add(observer);
   }
   public void unregisterObserver(Observer observer) {
      if (mObservers != null && observer != null) {
         mObservers.remove(observer);
      }
  }
  private void notifyObservers() {
     if (mObservers == null) {
        return;
     }
     for (Observer observer : mObservers) {
        observer.onObservableChanged();
     }
   }
}

 

코드가 적당히 길긴 한데 우리가 자세히 봐야할 것은 멤버변수로 Set을 선언했다는 것과 registerObserver메서드를 통해 HashSet구현체로 동적할당했다는 것을 주의깊게 보셔야 합니다. 

 

unregisterObserver와 notifyObservers 코드는 우리가 알아볼 동시성문제와 직접적인 연관은 없으니 위에서 언급한 두개만 주의깊게 보시면 됩니다. 

 

위와 같은 코드를 만든 상황에서 말그대로 동시에 A, B 두개의 스레드가 registerObserver에 접근했다고 가정해봅시다. 그럼 어떤 일이 일어날까요?

 

  1. 스레드 A가 registerObserver에 접근합니다. 
  2. 스레드 A가 파라미터로 넘어온 observer가 null인지 확인하고 우리가 선언한 mObservers도 null인 경우 HashSet으로 동적할당했습니다. 
  3. new 연산자가 실행되고 있던 도중 스레드 B가 접근하면 OS는 스레드 A의 작업을 잠시 중지하고 스레드 B를 실행시킵니다.
  4. 스레드 B가 1번, 2번의 과정을 거쳐서 mObservers를 HashSet으로 만듭니다. 
  5. 스레드 A가 mObservers를 아직 만들지 않은 상태에서 스레드 B가 새로운 Set을 만드는데 성공하고 mObservers에 레퍼런스들을 저장합니다. 
  6. 스레드 B에 Set이 새롭게 만들어집니다. 
  7. OS는 스레드 A의 작업을 다시 실행합니다. 
  8. 스레드 A가 새로운 Set을 만들고 mObservers에 레퍼런스들을 오버라이드합니다. 
  9. 스레드 A가 다시 Set을 새롭게 만들었습니다. 

 

이렇게 타임라인으로 알아봤는데요. 이런 상황이면 어떤 문제가 발생할까요? 

 

바로 스레드 B가 만든 Set은 스레드 A가 만든 Set에 덮어씌워집니다. 이런 상황을 OS용어로는 'race condition에 돌입했다' 라고 말합니다. race condition에 대해서 잠깐 설명하고 넘어가자면

 

race condition

race condition은 서로 다른 두 사람이 모든 형광등이 연결된 스위치를 켰다 껐다 하는 것으로 설명할 수 있을 것 같습니다. 

 

모든 형광등이 스위치에 연결되면 스위치를 키면 모든 형광등이 켜지고 스위치를 내리면 모든 형광등이 꺼지겠죠? 

 

위의 예제 코드는 한명은 스위치를 내리려고하고 한명은 스위치를 올릴려고 하는 상황입니다. 그럼 꺼지던 켜지던 한가지 상황만 발생하겠죠. 그럼 그 상황을 원치 않았던 한명이 생기는 것입니다. 

 

두 사람이 순서대로 읽고 저장하는 작업을 실행한 과정을 위의 예시에서 확인할 수 있습니다. 프로세스 1이 읽고 쓰고 얼마 있지않아 프로세스 2가 읽고 쓴 상황입니다. 정상적으로 잘 작동 하네요. 

 

이 상황은 어떤가요? 프로세스 1과 2가 서로 겹쳐서 작업이 꼬여버린 상황입니다. 이런 상황을 race condition이라고 합니다. 

 

 

동시성 문제를 해결하는 방법

먼저 동시성 문제가 어떤 상황에서 발생하는지 패턴을 몇개 알아두시면 좋습니다. 제가 알고있는 패턴을 소개해드리겠습니다. 

 

  1. 멤버변수 (전역변수) 를 선언한 경우
  2. Thread-safe 하지 않은 클래스를 사용한 경우
  3. ++, -- 와 같은 증감연산자를 사용한 경우
  4. 동시에 여러개의 스레드가 하나의 메서드에 접근하는 경우

 

대충 이정도로 정리할 수 있습니다. 그럼 이제 자바에서 어떻게 동시성문제를 해결하는지 알아봅시다. 

 

1. 불변객체를 사용한다. 

자바에서 불변객체를 사용하면 동시성 문제를 해결할 수 있습니다. 불변 객체는 한번 만들어지면 이후에 절대 변경되지 않는 특성을 가지고 있는데요. 예를 들어서 불변 객체인 String은 동시성 문제가 발생하지 않습니다. 

 

2. final 변수를 사용한다. 

final 변수는 자바에서 Thread-safe로 굉장히 유명합니다. final 변수에 대해서 잘 모르시는 분들은 아래의 링크를 확인해주세요. 

 

https://coding-review.tistory.com/156

 

final 키워드

자바에서 final 키워드는 상수를 표현하기 위한 예약어이다. '마지막' 이라는 단어 뜻처럼 선언한 그대로 사용하라는 의미이다. 변수, 메서드, 클래스에서 모두 이용되고 있는데 지금부터 각각의

coding-review.tistory.com

 

3. Read-only를 사용한다. 

단순히 읽는 작업만 필요한 경우에 Read-only를 사용하는 것도 좋은 방법입니다. 

 

4. synchronized 키워드를 사용한다. 

synchronized 키워드는 접근할 때 오직 하나의 스레드만 접근할 수 있도록하는 키워드입니다. synchronized 블록을 사용하면 Lock을 획득한 스레드만 접근이 가능해서 해당 스레드가 나가기 전까지 스레드가 뒤에서 대기할 수 밖에 없습니다. 

 

5. static 변수를 사용하지 않는다. 

static변수는 동기화가 되기 힘든 변수입니다. 만약 여러 스레드가 static 변수에 접근한다면 엄청난 버그가 생길 수 있으니 정말 변하지 않는 값만 static 변수로 설정하는 것이 좋습니다. 

 

6.  java.util.concurrent 클래스를 사용한다. 

concurrent 클래스는 Thread-safe를 보장하는 클래스입니다. concurrent 클래스는 내부적으로 Segment라고 하는 개념으로 Reentrant Lock을 걸어 Thread-safe를 보장합니다. 쉽게 설명하면 각각의 스레드에 Segment를 부여하고 Segment별로 병렬적으로 변수를 관리하기 때문에 동시성 문제가 발생하지 않는다고 보시면 됩니다. 

 

Concurrent 클래스에 대해서 자세히 알고 싶으신 분들은 아래의 링크를 확인해주세요!

https://coding-review.tistory.com/278

 

ConcurrentMap, ConcurrentHashMap

이번 포스팅은 기존 Map 인터페이스의 구현체 중 thread-safe 하다고 알려져있는 바로 그 구현체 ConcurrentMap입니다. 앞선 포스팅인 Map에 대한 기본적인 내용과 지금 쓰려는 이 내용은 지식의 깊이부

coding-review.tistory.com

 

7. 멤버변수(지역변수)를 사용하지 않는다. 

멤버변수를 굳이 사용하지 않아도 되는 경우에는 사용하지 않는 것 또한 동시성 문제를 해결하는 방법 중 하나입니다. 그럼 지역변수를 Thread-safe할까요? 

 

지역변수는 각각의 스레드가 Stack 저장소에 지역변수를 저장하고 이 Stack 메모리는 스레드별로 가지고 있는 저장소이기 때문에 다른 스레드가 접근할 수 없습니다. 

 

8. 멤버변수를 사용해야 한다면 ThreadLocal을 사용한다. 

ThreadLocal은 내부적으로 Map을 사용함으로써 변수마다 값을 고유하게 유지시켜주는 역할을 합니다. ThreadLocal에 대한 자세한 내용은 아래의 링크에서 확인해주세요!

 

https://coding-review.tistory.com/92

 

동시성문제와 스레드 로컬

이 포스팅은 인프런 김영한 님의 스프링 핵심 원리 고급 편을 보고 각색한 포스팅입니다. 자세한 내용은 강의를 확인해주세요 이번 시간에는 스프링을 사용할 때 주의할 점과 해결방법인 스레

coding-review.tistory.com

 

9. Volatile 키워드는 제한적으로 동시성문제를 해결해줍니다. 

모든 상황에서 Volatile 키워드가 Thread-safe 하지는 않습니다. 하지만 아주 제한적으로는 동시성 문제를 해결해주곤 합니다. 예를 들어서 두개의 스레드중 하나는 읽기 작업만 진행하고 하나는 쓰기 작업만 진행하는 경우 동시성 문제를 해결해줍니다. 

 

 

 

마치며

여기까지 동시성문제와 Thread-safe 를 알아봤고 해결방법까지 알아봤습니다. 동시성 문제는 개발자라면 항상 생각해야 할 문제이기 때문에 한번 알아두면 두고두고 쓸 일이 생기니 기억해두시면 좋습니다. 

 

저도 언젠간 동시성 문제를 진지하게 고민해볼 날이 왔으면 좋겠네요. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!

 

 

출처

https://gowthamy.medium.com/concurrent-programming-fundamentals-thread-safety-6b44c026bd2a

 

Concurrent Programming Fundamentals— Thread Safety

What is thread safety?

gowthamy.medium.com

=> Thread-safe에 대한 전반적인 이해와 해결방안

 

https://www.techtarget.com/searchstorage/definition/race-condition

 

What is a Race Condition?

Learn what race conditions are in computer science and programming, how they work, what security vulnerabilities they cause and how they can be prevented.

www.techtarget.com

=> race condition에 대한 전반적인 개념

 

https://www.baeldung.com/java-volatile-variables-thread-safety

=> volatile 키워드의 Thread-safe 여부에 대한 docs

'CS 지식 > 운영체제' 카테고리의 다른 글

리눅스 커널  (0) 2024.06.02
세마포어와 뮤텍스  (0) 2023.04.27
교착상태 (DeadLock) 와 기아상태 (Starvation)  (0) 2023.04.24
프로세스 동기화  (0) 2023.04.16
프로세스와 스레드  (0) 2023.04.08