데이터 경쟁
은 멀티 쓰레드 환경에서 같은 데이터 (메모리 주소)에 접근을 시도하는 경우 발생한다.
만약에 A의 쓰레드에서 숫자를 증가시키려 하고 B의 쓰레드에서 그것을 읽으려고 할 경우
데이터 경쟁이 발생한다.
go run -race .
을 통해
데이터 경쟁 상태인지 알 수 있다.
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에 대해 알아보겠다.
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
}
조금 복잡한 방법으로
이러한 경우 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
}
geth에서의 data race 이슈 : https://github.com/ethereum/go-ethereum/issues/24299
atmoic과 mutex는 모두 비슷한 작업을 수행하지만
atmoic이 속도가 조금 더 빠르다.
그리고 atmoic의 경우 쓰기연산에 대한 원자성을 보장을 해주지만
읽기 순서에 대해서는 그러하지 않다.
즉, &cnt
에 저장되는 값이 &cnt+1
되는 것은 보장 해주지만
다른 고루틴이 동시에 이 값을 읽으려 시도하는 경우에는
동일한 값인 &cnt+1
을 가져온다는 보장을 하지 않는다.
반면에 mutex의 경우 공유되는 값에 대한 접근의 엄격한 순서를 보장한다.
결론으로 atmoic은 작은 연산에 대해 사용하고
그 외의 경우 mutex를 사용하면 좋다.