Golang 데이터 경쟁

00_8_3·2023년 2월 17일
0

Go

목록 보기
2/10

Data Race이란

데이터 경쟁은 멀티 쓰레드 환경에서 같은 데이터 (메모리 주소)에 접근을 시도하는 경우 발생한다.

만약에 A의 쓰레드에서 숫자를 증가시키려 하고 B의 쓰레드에서 그것을 읽으려고 할 경우
데이터 경쟁이 발생한다.

go run -race . 을 통해
데이터 경쟁 상태인지 알 수 있다.

sync.WaitGroup

sync.WaitGroup은 모든 고루틴이 종료될 때까지 대기해야 할 때 사용한다.

• func (wg *WaitGroup) Add(delta int): WaitGroup에 대기 중인 고루틴 개수 추가

• func (wg *WaitGroup) Done(): 대기 중인 고루틴의 수행이 종료되는 것을 알려줌

• func (wg *WaitGroup) Wait(): 모든 고루틴이 종료될 때까지 대기

package main

import (
	"fmt"
	"sync"
)

func main() {
	var n int32
	var wg sync.WaitGroup
	wg.Add(2) // 고루틴 개수

	go func() { // 고루틴 1
		n++
		wg.Done()
	}()

	go func() { // 고루틴 2
		n++
		wg.Done()
	}()

	wg.Wait()

	fmt.Println(n) // 2
}

원자성 보장

위 코드를 for loop을 통해
n의 변수를 공유자원으로 사용하여
각 고루틴에서 10,000 씩 증분연산을 하여

20,000의 결과를 바라는 코드를 작성해 보았다.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var n int32
	var wg sync.WaitGroup

    for i := 0; i < 100000; i++ {
        wg.Add(2) // for loop 한 번에 고루틴 2개씩 추가.
        
        go func() { // 고루틴 1
            n++
            wg.Done()
        }()
    
        go func() { // 고루틴 2
            n++
            wg.Done()
        }()
}

	

	wg.Wait()
	fmt.Println(n) // 184939 // 각자 다를 수 있습니다.
}

원하는 출력 값은 20000 이지만 그 보다 작은 값이 출력되는 것을 보았을 때

n 변수에 대한 원자성이 보장받지 않는 것을 알 수 있다.

데이터 경쟁을 제거하는 방법 3가지 Mutex, Chan, Atomic 중
Mutex, Atomic에 대해 알아보겠다.

sync.atomic을 통한 원자성 보장

package main

import (
	"fmt"
	"sync/atomic"
	"time"
)

func main() {
	var ops uint64 = 0

    for i :=0; i < 10000; i++ {
        go func ()  {
            atomic.AddUint64(&ops, 1)

            time.Sleep(time.Microsecond)
        }()

        go func ()  {
            atomic.AddUint64(&ops, 1)

            time.Sleep(time.Microsecond)
        }()
    }

    time.Sleep(time.Second)

    opsFinal := atomic.LoadUint64(&ops)
    fmt.Println("ops :", opsFinal) // 20000
}

Mutex를 통한 원자성 보장

조금 복잡한 방법으로

이러한 경우 sync.Mutex를 사용하여 하나의 변수에 대한 상호배제를 통해 원자성을 보장 받을 수 있다.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var n int32
	var wg sync.WaitGroup
    var mu = sync.Mutex{}

    for i := 0; i < 100000; i++ {
        wg.Add(2)
        go func() { // 고루틴 1
            mu.Lock()
            n++
            mu.Unlock()
            wg.Done()
        }()
    
        go func() { // 고루틴 2
            mu.Lock()
            n++
            mu.Unlock()
            wg.Done()
        }()
}

	wg.Wait()

	fmt.Println(n) // 20000
}

참고

결론

atmoic과 mutex는 모두 비슷한 작업을 수행하지만

atmoic이 속도가 조금 더 빠르다.

그리고 atmoic의 경우 쓰기연산에 대한 원자성을 보장을 해주지만

읽기 순서에 대해서는 그러하지 않다.

즉, &cnt에 저장되는 값이 &cnt+1 되는 것은 보장 해주지만
다른 고루틴이 동시에 이 값을 읽으려 시도하는 경우에는
동일한 값인 &cnt+1을 가져온다는 보장을 하지 않는다.

반면에 mutex의 경우 공유되는 값에 대한 접근의 엄격한 순서를 보장한다.

결론으로 atmoic은 작은 연산에 대해 사용하고
그 외의 경우 mutex를 사용하면 좋다.

0개의 댓글