속도는 느려지고, 쓰기 위해서 이것 저것 해야해서 불편한 동기화를 대체 왜 해야하는 걸까?


동기화의 이유는 순서 또는 공유 자원 때문이다.

순서는 말 그대로.. A가 처리 된 이후에 B가 처리되어야하는 상황(입금이 되면 상품을 발송할 게 같은.)일 때 두 작업을 처리하는 작업자가 서로 다르다면 순서가 안 맞는 (입금이 안됬어도 상품을 줄게) 상황이 일어날 수 있으므로.. 순서를 맞출 때 쓰는 것이다.


두 작업자가 하나의 자원에 동시 접근(Write / Read)하는 것인데...

그림은 그냥 동시에 접근하는 것 처럼 보이지만 실제로는 한 대가 다닐 수 있는 병목 구간에 두 자동차가 같이 들어가려는 것 같은 상황이다.


사실 컴퓨터가 작업을 동시에 처리하는 능력이 없고, 하나 씩 처리한다는 것에 비춰볼 때.. 순서가 있다면 그렇다면 사실상 '동시 접근' 은 없는 게 아닌가? 하는 생각이 들었다.


동시 접근이 없다? 그런데 왜 문제가?..



OS가 행하는 스케쥴링은 각 작업자에게 시간을 잘게 잘라서(time slice) 분배하는데.. 위 같은 경우 작업자 A가 공유자원을 미처 다 쓰지 못했을 때, B가 바로 들어오게 된다. 그러니까 상황이 이렇게 된다.

빨간 선 간의 간격이 작업을 하게 지급된 퀀텀(quantum)이라고 가정했을 때,

위와 같이 작업을 하러 들어와서 A를 처리하고 B를 처리한다면 공유자원 문제는 절대로 발생하지도 않을 것이고, 모든 동기화 기법들은 장사를 접어야한다.


그런데 현실은 녹록치 않아서 (단위가 무려 천분의 1초인 millisecond를 10 ~ 20초 지급 받는다) 간격은 더럽게 짧고, 해당 간격이 끝났다면 다음 작업자 (B)에게 넘어가는 식이다.

즉 A - B - A - B - A - B 로 작업이 끝나게 된다.


물론 이 상황은 스케쥴러의 정책에 따라 다르다. 라운드 로빈 같은 경우는 다 똑같이 공평한 시간이 분배되고, HRN 같은 경우 긴 작업과 짧은 작업에 차등성을 부여하는 등 차이가 있을 수 있다.



아무튼 문제가 되는 것은

ABAB.. 로 시작되었을 때 이다. 공유자원이 문제라고 했는데..

A -> B -> B 로 돌아오는 이 과정의 사이 사이에는 문맥[상황] 교환(context switching)이 일어나게 된다. 



이처럼 시간이 지날 때마다 CPU는 A - B - A - B로 작업자가 전환되게 된다. 

문제는 작업이 전부 끝나지 않았을 때, 쓰레드에게 모든 했던 처리를 버리라고하면?

인셉션이 된다. A(10%) - [A버림] > B(10%) - [B버림] -> A(다시 10%) ...


당연히 처리해놓았던 것을 저장해두어야하고, 그것을 메모리 내에 저장해두게 된다. 그리고서는 다시 레지스터에 올려 작업을 시작하게 되는 데...


우리가 두 스레드 A , B 를 통해 공유 자원(주로 데이터 영역)을 변경한다면

A B가 작업하고 난 뒤에 우리가 원하는 형태는


A와 B의 작업이 완료된 공유자원. 의 상태일 것이다.



[ 공유자원 ]

[ A의 작업이 반영된 공유자원 ] <- A 작업 끝

[ A의 작업이 반영된 B의 작업이 반영된 공유자원] < - B 작업 끝



하지만 현실은..


[ 공유자원 ]

 < --- 공유 자원을 가져온 A의 작업 중...

    ㄴ 인터럽트 / 스레드 B 실행

       ㄴ A의 상태 저장 (context switching)

[ 공유자원 ]

 < --- 공유 자원을 가져온 B의 작업 중...

    ㄴ 인터럽트 / 스레드 A 실행

      ㄴ B의 상태 저장 (context switching)

 < --- A의 작업을 반영

[ A의 작업이 반영된 공유자원 ]

 < --- B의 작업을 반영

[ B의 작업이 반영된 공유자원 ]



...?!


최종적으로 B의 작업이 반영된 공유자원이 남았다.


이는 B가 [A의 작업이 반영된 공유자원]을 가져간 게 아니기 때문이다.


[ 공유자원 ]

- > 공유자원을 가져온 A의 작업 시작

[ A의 작업이 반영된 공유자원 ] <- A 작업 끝

- > A의 작업이 반영된 공유자원을 가져온 B의 작업 시작

[ A의 작업이 반영된 B의 작업이 반영된 공유자원] < - B 작업 끝


의 구조로 이루어져야하는 절차가


[공유자원 ]

- > 공유자원을 가져온 A의 작업 시작

  ㄴ interrupt / context switching

- > 공유자원을 가져온 B의 작업 시작

[ A의 작업이 반영된 공유자원 ] <- A 작업 끝

[ B의 작업이 반영된 공유자원 ] <- B 작업 끝


와 같이 되기 때문이다.


물론 스레드 예제를 실행시켜보면 알겠지만.
ABABABABABAB 가 아니라.
AAAABBBABABBBAB 라던가 ABAABBAABBBBAA처럼
균일하지 않게 스레드가 실행되므로 위처럼 순서가 지켜지지 않고
B나 A가 연속적으로 처리되는 일도 있을 수 있다.

이런 것을 비 결정론적 성질(버그)이라고 하는데.. 간단히 말해서 할 때마다 다르다는 얘기다.

우리가 원하는 

[ A의 작업이 반영된 B의 작업이 반영된 공유자원 ] 이라는 상태는

AAA BBB 해서 작업이 끝나버린다면 완벽하게 반영될 것이고... 

이는 오류 없음 의 결과를 도출하게 된다.


그렇지만 스케쥴링이 ABABAB 라던가 AABBBA 같이 된다면 결과는 예측할 수 없게 된다.


비단 멀티 스레드만이 아니라 멀티 프로세스 환경 등에서도 일어날 수 있다. 공유자원을 두고 둘 이상의 작업자가 진입하면 일어날 수 있는 현상인 것이다. 이를 경쟁 상태(race condition)이라고 한덴다...


아무튼 그래서 둘 이상의 작업자가 공유자원에 접근하게 되는 환경에서는 연산이 섞이는 일이 없도록 원자적 연산(atomic operation)을 보장해주거나 [이 경우 값 넣고 있는 동안은 세트로 묶여서 섞이지 않는 범위 까지 연산을 보장해준다]


동기화 기법을 쓰게 되는 것이다.







Posted by GENESIS8

댓글을 달아 주세요

  1. juicyjerry 2020.12.16 00:42 신고  댓글주소  수정/삭제  댓글쓰기

    정리를 잘 해주셔서 잘 읽었습니다.
    유익한 정보 잘 보고갑니다~ :)