CS

[운영체제] 동시성 이슈란 - Race Condition/Data Race

nkdev 2024. 12. 23. 22:55

멀티 스레드 환경의 동시성 문제

멀티 스레드 환경에서는 공유 자원에 여러 스레드가 동시에 접근하려고 경쟁하게 되는데, 이를 '동시성 문제'라고 한다. 동시성 문제의 예시로는 레이스 컨디션, 데이터 레이스가 있다.

 

1. 레이스 컨디션 (race condition)

스레드가 실행되는 순서가 항상 보장되지 않아서 발생하는 문제. 여러 스레드가 공유 자원을 병행적으로(concurrently) 읽거나 쓸 때 자원에 접근한 순서에 따라 그 실행 결과가 바뀌는 상황이다. 

 

(예제 1)

아래 코드를 보면 main() 고루틴과 func() 고루틴이 멀티 스레드로 실행된다.

그런데 만약 (1) -> (2) 순으로 수행되었다면 아무것도 출력되지 않을 것이고, (2) -> (1) 순으로 수행되었다면 'the value is 1'이라고 출력될 것이다. 

package main

import (
	"fmt"
	_ "time"
)

func main() {
	var data int

	go func() { 
		data++ // 작업 (1)
	}()

	if data == 0 { // 작업 (2)
		fmt.Printf("the value is %v.\n", data)
	}
}

(예제 2)

이 경우도 마찬가지로 main()과 func() 고루틴이 멀티 스레드로 실행된다.

만약 (1) -> (2) 순서로 수행되었다면 'done은 true입니다.'가 출력되었겠지만 (2)가 먼저 수행되면 프로세스가 무한 루프에 빠질 것이다.

package main

import (
	"fmt"
	"time"
)

func main() {
	done := false

	go func() {
		done = true // 작업 (1)
	}()

	for !done { // 작업 (2) //done이 true가 되길 기다림
	}

	fmt.Println("done은 true입니다.") 
}

 

2. 데이터 레이스 (data race)

공유 자원의 데이터 값을 읽고 쓰는 작업이 동시에 일어나서 발생하는 문제. 한 스레드가 값을 읽은 후 연산 결과를 아직 쓰지 않았는데 다른 스레드에서 값을 또 읽고 작업해서 쓰기 연산의 결과가 덮어씌워지는 상황이다. 공유 자원에 대한 연산이 원자성(atomicity)을 만족하지 않으면 발생한다. (레이스 컨디션의 하위 개념에 해당하며 메모리의 접근에 조금 더 초점이 맞춰져있다.)

 

(예제)

아래 코드에서 두 개의 고루틴은 멀티 스레드로 counter이라는 공유 자원을 1씩 증가시키는 연산을 수행한다.

package main

import (
	"fmt"
	"sync"
	_ "time"
)

func main() {
	var counter int

	var wg sync.WaitGroup
	var inc = func(count int) {
		defer wg.Done()
		for i := 0; i < count; i++ {
			counter++ // 공유 자원 //원자적이지 않은 연산 
		}
	}

	wg.Add(2)
	go inc(5000) // 고루틴 1 : counter 변수를 5000번 증가시킴
	go inc(5000) // 고루틴 2 : counter 변수를 5000번 증가시킴 
	wg.Wait()

	fmt.Println("Count: ", counter)
}

//Count:  8744

 

고루틴 1과 고루틴2는 각각 counter값을 5000번 증가시키는 연산을 수행했지만 실행 결과 counter값은 10000이 아니라 8744가 나온다. 이는 동시성 프로그래밍에서 데이터 레이스(data race)가 발생했을 때, 데이터에 대한 연산이 원자성(atomicity)을 만족하지 않기 때문에 나타난 결과이다. 

 

counter++이라는 연산은 원자적이지 않다. 실제로는 이 연산을 세 가지 단계로 분리할 수 있다.

  • 읽기 : counter값을 가져옴
  • 증가 연산 : counter값을 증가시킴
  • 쓰기 : counter값을 저장

따라서 다음과 같은 문제가 발생한다.

  1. 고루틴 1이 counter값을 읽고(counter == 0) 고루틴 2도 동시에 counter값을 읽었다.(counter == 0)
  2. 고루틴 1이 읽은 값에 1을 더하고 counter 변수에 저장했다. (counter = 1)
  3. 고루틴 2도 읽은 값에 1을 더하고 counter 변수에 저장했다. (counter = 1 --> 고루틴 1의 연산 결과를 덮어씀)

고루틴 1의 counter++작업이 다 완료된 후 고루틴 2의 counter++작업이 시작될 줄 알았는데 알고보니 counter++는 읽기->증가 연산->쓰기 세 단계로 실행되는 작업이었고 이런 원자적인 작업이 실행되는 순서가 항상 보장되지 않아서 잘못된 연산 결과가 도출되었다.

 

참고로 이러한 문제가 발생하는 이유는 여러가지가 있을 수 있다. 한 스레드가 cpu에서 연산하는 도중에 외부 i/o장치에서 interrupt가 발생하여 값을 읽기만 한 상태에서 context switching된 상황이라든지, 멀티 코어 환경에서 여러 cpu가 동시에 같은 메모리 주소를 읽었다든지 하는 상황을 예상해볼 수 있다.

동시성 문제의 해결 방법

이러한 동시성 문제를 해결하기 위해서는 다음과 같은 방법을 사용할 수 있다.

  • 작업 간 실행 순서를 조율해서 공유 자원에 대한 접근을 직렬화하기
  • Semaphore/Mutex를 사용해서 공유 자원에 한 번에 하나의 스레드만 접근할 수 있게 보장하기