Posts [운영체제] 스레드와 프로세스
Post
Cancel

[운영체제] 스레드와 프로세스

프로그램

  • 일반적으로 특정 작업을 수행하는 소프트웨어

프로세스

  • 메모리나 CPU와 같은 자원을 할당받아 실행 중인 프로그램
  • 독자적인 메모리를 할당받아서 서로 다른 프로세스끼리는 일반적으로 서로의 메모리 영역을 침범하지 못함

스레드:

  • 프로세스를 구성하는 하나의 단위, 작업의 실행 단위
  • 하나의 프로세스는 여러 스레드가 작동하고 있을 수 있음
  • 하나의 프로세스에 존재하는 여러 스레드들은 같은 자원을 공유하여 사용 할 수 있음
  • 이로 인해 병렬성의 향상이라는 장점이 있는 반면에 동시성 문제, 데드락과 같은 여러 가지 문제점도 발생 할 수 있음
  • 여러 스레드가 작동하는 환경에서도 문제 없이 동작하는 것을 스레드 안전(Thread-safe)하다고 말 할 수 있음

스레드 안정성이 깨지는 상황

  • 100명의 스레드로 각각 100번 조회하는 프로그램
  • 소스코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CountingTest {
    public static void main(String[] args) {
        Count count = new Count();
        for (int i = 0; i < 100; i++) {
            new Thread(){
                public void run(){
                    for (int j = 0; j < 100; j++) {
                        System.out.println(count.view());
                    }
                }
            }.start();
        }
    }
}
class Count {
    private int count;
    public int view() {return count++;}
    public int getCount() {return count;}
}
  • 실행 결과
    • 해당 코드를 실행시켰을 때, 100명의 사용자가 100번 조회했으므로 100 * 100, 즉 10000번의 조회수가 나오지 않고 그보다 더 적은 조회수가 나옴
    • 조회수를 증가시키는 로직이 두 번의 동작으로 이뤄어지는데 동시에 여러 스레드가 접근하여 첫 번째 동작할 때의 자원과 두 번째 동작할 때의 자원 상태가 변하기 때문(동시성 이슈 발생)
    • 444
    • 여러 스레드에서 동시에 count변수에 접근한다면 동시에 1번 동작을 진행하여 같은 count값을 조회할 것이고 두 개의 스레드가 1을 더하는 조회 로직을 실행한다 해도 2가 더해지는 것이 아닌 1이 더해지게 됨

스레드 안정성을 유지하는 방법 (동시성을 제어하는 방법)

5

1) 암시적 Lock

  • lock을 적용하게 되면 하나의 스레드가 해당 메서드를 실행하고 있을 때 다른 메서드가 해당 메서드를 실행하지 못하고 대기하게 됨(Java에서 synchronized키워드 사용)
  • 즉, 한 번에 하나의 스레드만 접근할 수 있게 됨
  • 하지만 한 번에 하나의 스레드만 접근하다보니 병렬성을 매우 낮아지게 되고 결과적으로 프로그램의 퍼포먼스가 감소하게 됨
  • lock은 메서드, 변수에 각각 걸 수 있음
    • 1) 메서드에 lock을 거는 방법
    1
    2
    3
    4
    
      class Count {
          private int count;
          public synchronized int view() {return count++;}
      }
    
    • 2) 변수에 lock을 거는 방법
    1
    2
    3
    4
    5
    6
    7
    8
    
      class Count {
          private Integer count = 0;
          public int view() {
              synchronized (this.count) {
                  return count++;
              }
          }
      }
    
    • 상황에 따라 변수에 lock을 사용하거나 메소드에 lock을 사용하는데 단순하게 메서드에 Lock을 거는 방법 보단 특정 변수에 lock을 사용함으로써 스레드가 해당 로직을 수행하는데 있어 퍼포먼스를 높일 수 있음

2) 명시적 Lock

  • synchronized 키워드 없이 명시적으로 ReentrantLock을 사용하는 Lock
  • 해당 Lock의 범위를 메서드 내부에서 한정하기 어렵거나, 동시에 여러 Lock을 사용하고 싶을 때 사용
  • 직접적으로 Lock 객체를 생성하여 사용할 수 있음
  • lock() 메서드를 사용할 경우 다른 스레드가 해당 lock() 메서드 시작점에 접근하지 못하고 대기하게 됨
  • unlock() 메서드를 실행해야 다른 메서드가 lock을 획득 할 수 있게 됨
  • 예제 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CountingTest {
    public static void main(String[] args) {
        Count count = new Count();
        for (int i = 0; i < 100; i++) {
            new Thread(){
                public void run(){
                    for (int j = 0; j <b 1000; j++) {
                        count.getLock().lock();
                        System.out.println(count.view());
                        count.getLock().unlock();
                    }
                }
            }.start();
        }
    }
}
class Count {
    private int count = 0;
    private Lock lock = new ReentrantLock();
    public int view() {
            return count++;
    }
    public Lock getLock(){
        return lock;
    };
}

자원의 가시성을 책임지는 volatile

  • 여러 스레드가 하나의 자원에 동시에 read&write를 진행할 때 항상 메모리에 접근하지는 않음
  • 성능의 향상을 위해 CPU 캐시에 해당 값을 저장하는 방법을 사용하기 때문에 해당 데이터가 메모리에 저장된 실제 데이터와 항상 일치하는지 보장 할 수 없음
  • 즉, 변수에 저장한 데이터를 읽었는데 실제 메모리에 저장되어있는 데이터와 차이가 있을 수 있다는 것!
  • 메인 메모리에 저장된 실제 자원의 값을 볼 수 있는 개념을 자원의 가시성이라 하는데, 이 가시성을 확보하지 못한 것
  • volatile은 이러한 CPU 캐시 사용을 막음
  • 해당 변수에 volatile 키워드를 붙여주면 해당 변수는 캐시에 저장되는 대상에서 제외됨
  • 매 번 메모리에 접근해서 실제 값을 읽어오도록 설정해서 캐시 사용으로 인한 데이터 불일치 방지함
  • 실제 메모리에 저장된 값을 조회하고 이를 통해 자원의 가시성을 확보할 수 있음
  • volatile은 자원의 가시성을 확보해주지만 동시성 이슈를 해결하기에는 그리 충분하지 않음
  • 공유 자원에 read&write를 할 때는 동기화를 통해 해당 연산이 원자성을 이루도록 설정해주어야 동시성 이슈를 해결할 수 있음
  • volatile이 효과적인 경우는 하나의 스레드가 wtite를 하고 다른 하나의 스레드가 read만 할 경우입니다. 이 경우 read만 하는 스레드는 CPU 캐시를 사용하고 다른 스레드가 write한 값을 즉각적으로 확인하지 못함

스레드 안전한 객체 사용

  • Concurrent 패키지
    • AtomicInteger와 같은 클래스는 i++와 같은 연산을 단일 연산으로 만든 메서드를 제공해줌
    • 해당 클래스의 메서드는 내부적으로 Thread-safe 하게 구조화되어 있어서 스레드 안전한 프로그램을 만드는 것에 도움을 줄 수 있음
    1
    2
    3
    4
    5
    6
    
      class Count {
      private AtomicInteger count = new AtomicInteger(0);
      public int view() {
              return count.getAndIncrement();
      }
      }
    
  • ConcurrentHashMap
    • concurrent패키지에 존재하는 컬랙션들은 락을 사용할 때 발생하는 성능 저하를 최소한으로 만들어두었음
    • 락을 여러 개로 분할하여 사용하는 Lock Striping 기법을 사용하여 동시에 여러 스레드가 하나의 자원에 접근하더라도 동시성 이슈가 발생하지 않도록 도와줌
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
      int binCount = 0;
          for (Node<K,V>[] tab = table;;) {
              Node<K,V> f; int n, i, fh;
              if (tab == null || (n = tab.length) == 0)
                  tab = initTable();
              else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                  if (casTabAt(tab, i, null,
                               new Node<K,V>(hash, key, value, null)))
                      break;                   // no lock when adding to empty bin
              }
              else if ((fh = f.hash) == MOVED)
                  tab = helpTransfer(tab, f);
              else {
                  V oldVal = null;
                  synchronized (f) {
                      if (tabAt(tab, i) == f) {
    
    • ConcurrentHashMap은 내부적으로 여러개의 락을 가지고 해시값을 이용해 이러한 락을 분할하여 사용
  • 불변 객체 (Immutable Instance)
    • String 객체처럼 한번 만들면 그 상태가 변하지 않는 객체를 불변객체라고 함
    • 불변 객체는 락을 걸 필요가 없습니다. 내부적인 상태가 변하지 않으니 여러 스레드에서 동시에 참조해도 동시성 이슈가 발생하지 않는다는 장점이 있음
    • 즉, 불변 객체는 언제라도 Thread-safe함
    • 객체의 상태가 변화되지 않도록 setter메소드를 만들지 않고 내부의 모든 변수를 final로 선언하면 됨

출처

This post is licensed under CC BY 4.0 by the author.