BASIC의 개발 노트

Concurrency - Race Condition 본문

OS

Concurrency - Race Condition

B2SIC 2021. 6. 4. 04:36

< 이 글은 'Concurrency' 글과 연결됩니다. >

 

Thread를 사용할 때 공유된 자원(데이터)이 있다면 어떻게 될까? -> 문제가 발생한다.

이전 글 Concurrency 에서 봤던 코드와는 조금 다른데, mythread에서 counter 라는 변수를 증가 시키는 일을 한다.

그런데 counter는 전역 변수로 선언이 됐다. (volatile은 최적화(레지스터 이용 등)하지 않고 메모리에 머물게 하겠다)

static은 이 코드에서만 counter를 사용하겠다는 뜻, 다른 코드에서는 counter를 다른 의미로 써도 무방.

전역 변수는 하나인데 Thread를 2개 생성해서 각 thread 들이 counter 변수를 공유하게 됐다.

 

여기서 짚고 넘어가야 할 내용이 있다.

하나의 Process에서 Thread가 서로 공유하는 자원에는 무엇이 있을까?

Data 영역, Heap 공간, Code, File Descriptor Table 등이 있다.

그러나 TCB와 Stack은 thread 별로 따로 갖는다. (이 외는 대부분 공유)

이 의미는 전역 변수인 counter는 Data 영역에 들어가서 T1과 T2가 공유하게 되고,

지역변수는 각 thread의 stack에 들어가서 서로 다른 상태로 존재한다.

 

위 코드의 결과 우리가 기대하는 것은 Thread1에서 counter를 1e7, 즉 1000만 만큼 증가 시키고

Thread2에서 또 1000만 만큼 증가시키기 때문에 2000만이라는 값이 counter에 써질 것을 기대한다.

좀 더 일반화하면 같은 일을 둘에게 시켰기 때문에 두 배로 값이 증가했을 것이다 라고 기대한다.

 

하지만 실행을 해보면...

counter 값이 2000만에 못미친다.

 

그래서 다시 실행을 해봐도..

2000만이 되지 않고 오히려 전과 비교해서 값이 바뀌기 까지 했다.

이를 보고 결정적이지 않다 라고 해서 indeterministic 하다 라고 표현한다.

 

왜 이런 문제가 발생했을까?

우선 counter를 증가시키는 코드를 좀 더 low level에서 볼 필요가 있다.

이 세 줄이 counter++과 동일한 일을 한다.

뜯어보면 0x8049alc에서 %eax로 값을 loading하고, 1만큼 증가시켜준 다음, %eax 값을 0x8049alc에 다시 써준다.

이 과정을 각각 load, add, store 라고 한다. 이들은 앞으로 각 코드를 지칭하는 단어가 될테니 잘 보도록 하자.

 

그렇다면 이제 어떤 경우에 문제가 발생했는지 보자

여러 가지 경우가 있겠지만, 이 경우 Thread1에서 load, add를 실행하고 interrupt가 걸렸다.

그래서 OS가 T1에서 T2로 Context Switching을 했고, T2로 와서 다시 load, add, store를 하고 난 후

다시 interrupt가 걸려서 T1으로 왔다. 이전에 interrupt 되기 전에 해야했던 일이 store 였기 때문에

다시 Running이 됐을 때 T1은 store부터 하게 되는데 store를 하고 난 값을 보니 뭔가 이상함을 느낄 수 있다.

 

이번에는 PC값, eax값, counter 값에 집중해서 다시 보면

기존에 counter에 들어있던 50이라는 값을 가져와서 eax에 저장하고 1만큼 더해준 뒤 counter에 써야했지만

interrupt가 걸리는 바람에 그러지 못했다. 그 상태에서 T2로 넘어왔는데, PC가 100번지를 가리키고 있고

eax는 Thread2를 위한 Register 값으로 복원된 상태로 시작하기 때문에 0으로 시작한다.

즉 T2를 위한 Register 값이 복원 됐을 때 그 값은 0이었다는 뜻이다.

그래서 다시 counter 값을 eax로 가져오고 1만큼 더해줘서 counter에 저장한 다음 interrupt가 걸렸다.

T1으로 돌아오기전 T2의 내용을 save 하고 T1의 내용을 가져오는데, 마지막으로 interrupt가 걸리기 전에

eax에 1을 더한 51이 저장되어 있었기 때문에 eax값을 복원해서 51이 됐다. (T2의 51이 아님에 주의)

(이 때 주의할 것은 Register만 복원이 된다. counter는 메모리에 있는 것이기 때문에 저장된 상태로 유지된다.)

 

이후 T1이 할 일은 store이기 때문에 counter에 51이라는 값을 썼다.

하지만 T1도 1을 더해서 쓰고 T2도 1을 더해서 썼지만 결과는 50에서 51이 됐다.

 

그래서 이를 어떻게 해결해야할까?

더 이상 쪼갤 수 없다는 뜻을 가지고 있는 단어인, Atomicity 가 키워드이다.

 

그것은 바로 3줄로 된 기계어를 쪼갤 수 없도록 Atomic 하게 만드는 것이다.

또는 counter를 더하는 것을 기계어 하나로 사용하는 것도 해결 방법이 될 수 있다.

결국 문제의 핵심은 값을 불러와서 덧셈을 하고 메모리에 반영하기까지의 과정이 중간에 끊어져선 안된다는 것에 있다.

 

위의 예시에서 보면 T2는 load, add, store가 쪼개지지 않고 실행이 됐지만 T1에서는 쪼개졌다.

따라서 T1도 T2처럼 쪼개지지 않고 실행되도록 만들어야한다.

 

이러한 문제는 앞으로 계속 다룰 문제이기 때문에 이쯤에서 관련된 용어를 정리 하고 갈 필요가 있다.

 

Critical Section: Resource를 공유하는(그래서 상태를 바꾸는) 코드 조각을 말한다.

Critical Section 예시

 

Race Condition: 여러 개의 Thread가 Critical Section을 경쟁적으로 실행하다보니 동시에 상태를 바꾸려고 하는 것.

indeterminate: Race Condition에 의해서 결과가 매번 다르게 나오는 상황 (비결정적) -> 디버깅을 힘들게 한다.

Mutual Exclusion: 앞으로 배울 Lock 또는 Semaphore 처럼 해당 Critical Section이 상호배제 되게 동작하도록,

즉 어느 한 순간에 하나의 Thread만이 Critical Section을 실행할 수 있도록 해주는 것.

'OS' 카테고리의 다른 글

Concurrency - Quiz  (0) 2021.06.05
Concurrency - Multi Processing vs Multi Threading  (0) 2021.06.05
Concurrency  (0) 2021.06.04
File System - Read & Write + Directory  (0) 2021.05.26
File System  (0) 2021.05.25
Comments