동기화

김시환·2023년 3월 30일
0

운영체제

목록 보기
5/7

이전 글(프로세스와 스레드)에서 기본적인 프로세스와 스레드의 차이 그리고 멀티스레드와 멀티프로세스의 차이를 알아봤다.

결국 한 프로세스내에서 스레드는 메모리를 공유하기 때문에 병렬 처리를 할 때, 속도가 메모리 사용 측면에서 강점을 가진다. 하지만, 스레드를 제대로 사용하기 위해서는 몇 가지를 제대로 고려해야한다.

스레드 사용 시 발생하는 문제점

  1. 개발자가 실행 순서를 정확히 알지 못한다

    2개의 스레드가 존재하고, 각 스레드는 counter에 1을 더한 후 저장하는 작업을 수행한다. counter = counter + 1은 사실 원자적으로 수행되지 않는다. 기계어로 바꾸어 보면, counter에 있는 값을 eax레지스터에 옮기고, eax레지스터 값을 1 증가시킨 후, eax값을 다시 counter에 넣는다. 이렇게 3개의 기계어의 조합으로 코드가 작동된다.
    그런데 기계어를 실행하는 도중에 context switch가 일어난다면?
    => 위 사진처럼 counter의 값이 기댓값인 52가 아니라 51이 나온다. 스레드 별로 레지스터를 독립적으로 보유하고 있다. 스레드1에서 eax레지스터 값을 증가시켜 51이 되었지만, 그와 동시에 context switch가 발생하여 CPU제어권이 스레드2로 넘어갔다. 스레드2에서 3개의 기계어를 모두 수행하면, counter에는 51이 들어가 있다. 그 후 스레드1로 CPU제어권이 다시 넘어온 뒤, 스레드1의 eax레지스터에 있는 51을 counter로 옮기면 counter는 51이다.
    => 개발자는 counter가 52가 되기를 바랬을 것이다. 하지만 원하는대로 코드가 작동되지 않는다.

  2. 프로세스 내 주소 공간을 공유하기 때문에 critical section이 존재하고 이로 인해 race condition이 발생한다.
    위 그림의 문제의 원인은 counter라는 변수를 스레드들이 공유해서 사용하고 있다는 것이다. 공유 변수에 접근하는 부분을 critical section이라고 한다. 여러 스레드들이 critical section에 접근할 때 발생하는 문제를 race condition이라고 한다.

이 문제점들을 해결하기 위해서는 명령어들이 원자적으로 실행되어야 한다. 이를 구현하기 위해서 lock이라는 개념이 등장한다.

Lock

Controlling Interrupts

명령어의 원자성을 해치는 요소 중 하나가 interrupt다. critical section에 있을 때 interrupt를 받지 않도록 해버린다면?
=> 원자성을 보장할 수는 있다.

문제점

  1. interrupt를 활성화/비활성화 하는 것이 load가 많이 든다.
  2. 활성화/비활성화 하는 스레드에 대한 100% 신뢰가 필요하다. 악의적 사용자가 CPU제어권을 독점할 수도 있다.
  3. 멀티 프로세서에서는 동작하지 않는다. 다른 CPU의 스레드에서 critical section에 접근한다면, 스레드의 인터럽트 비활성화는 의미가 없다.
  4. 인터럽트를 받지 않는 것 자체가 문제! ex) I/O

Using Simple Flag

lock이 되었는지 아닌지를 보관하는 flag 사용
lock이 걸리면, flag 1로 설정
lock을 가지지 않는 스레드가 CPU 제어권을 가졌을 때, flag가 1이라면 무한 루프를 돌면서 대기
lock을 해제하면, flag 0으로 설정

문제점

  1. lock을 걸고 flag를 1로 설정하기 전에 context switch가 발생할 수도 있다!!
  2. spin-waiting : while문을 사용해서 계속 대기하는 것이 비효율적이다!

Test And Set

원자적으로 lock과 flag를 동기화할 수가 있다
이로써 단순히 flag만 사용했을 때 발생하는 문제점1을 해결할 수 있다. 하지만 여전히 spin-waiting은 존재한다.

지금까지 구현한 lock을 평가해보자.
1. 정확성 : mutual exclusion을 구현할 수 있다
2. 공평성 : spin-lock은 공평함을 보장하지 못한다.
3. 성능 : spin-lock은 CPU를 계속 사용하면서 대기하는 것이므로 성능 bad

Fetch And Add

fairness를 보장하기 위해 등장!
원자적으로 값을 증가시킬 수 있다
lock을 호출하면 해당 스레드는 Fetch And Add를 통해서 기존 티켓 넘버를 가지고 티켓 넘버를 1증가시키는 것을 원자적으로 할 수가 있다! lock을 가졌던 스레드가 unlock을 하면 turn 값이 1씩 증가한다. 이때도 Fetch And Add사용함. turn 값과 같은 티켓 넘버를 가진 스레드가 lock을 얻게 됩니다. -> 모든 스레드 실행 보장(fairness)

하지만 여전히 spin-waiting이 발생하고 있다. -> OS의 도움 필요!

Just Yield

spin이 발생하면 다른 스레드들이 실행될 수 있도록 CPU제어권을 양보한다!

문제점

  1. 여러 개의 스레드가 같은 lock을 얻으려 한다면, 계속 context switch 발생!!
  2. fairness 보장 x

Using Queues

lock을 가진 스레드가 unlock을 할 때, 다음 lock을 얻을 스레드를 지정!
스레드가 lock을 얻지 못한다면 큐에 추가!

문제점

여전히 spin-waiting이 아예 없지는 않다. 하지만, 스레드들이 큐에 보관되기 때문에 spin-wait 할 때 쓰는 시간을 줄일 수 있다!

Semaphore

Semaphore : Lock과 동작 원리는 같지만, sem이라는 변수를 0과 1이 아니라 그 이상의 정수로 설정할 수 있게 하여 하나 이상의 스레드가 critical section에 접근할 수 있게 한다

lock과 unlock을 사용하는 것이 아니라 sem_wait와 sem_post를 사용하여 sem의 값을 조절한다.
sem_wait : sem 1 감소, sem <= 0 라면, 큐에서 대기
sem_post : sem 1 증가

Lock과는 달리 Semphore는 락 해제의 주체와 락 획득의 주체가 같지 않을 수도 있다!

profile
1년차 개발자입니다.

0개의 댓글

관련 채용 정보