Mutex with Go

didnlie23·2023년 3월 27일
0
post-thumbnail

Mutex는 Mutual Exclusion의 줄임말이다. Mutex는 보통 여러 Thread들이 공유 데이터에 쓰기 작업을 동시에 시도하는 상황에서 의도치 않은 결과가 나오는 것을 방지하기 위해 사용된다.

쓰기 작업 시 연산의 대상이 되는 데이터를 가져올 때, 아직 업데이트 되지 않은 데이터를 가져오면 최종 결과에 영향을 미치게 된다.

Go에서는 sync.Mutex 타입의 변수의 메서드인 Lock()Unlock()를 호출해 동시에 2개 이상의 Goroutine이 접근할 수 없도록 Critical Section을 조성할 수 있다. 하나의 Goroutine이 Lock을 가지고 있는 상태라면, 다른 Goroutine들은 해당 Goroutine이 Lock을 반납할 때까지 기다려야 한다.

import (
	"github.com/stretchr/testify/assert"
	"sync"
	"testing"
)

func TestMutex(t *testing.T) {
	numGoroutine := 10000
	target := 0

	group := sync.WaitGroup{}
	for i := 0; i < numGoroutine; i++ {
		group.Add(1)
		go func() {
			defer group.Done()
			counter(&target)
		}()
	}
	group.Wait()

	assert.Equal(t, numGoroutine, target)
}

func counter(target *int) {
	*target++
}
=== RUN   TestMutex
    mutex_test.go:23: 
        	Error Trace:	/home/gyun/playground/channel/mutex_test.go:23
        	Error:      	Not equal: 
        	            	expected: 10000
        	            	actual  : 9262
        	Test:       	TestMutex
--- FAIL: TestMutex (0.00s)


Expected :10000
Actual   :9262

위의 테스트 코드에서 Mutex를 사용하지 않았을 때 발생할 수 있는 문제를 볼 수 있다.

import (
	"github.com/stretchr/testify/assert"
	"sync"
	"testing"
)

func TestMutex(t *testing.T) {
	numGoroutine := 10000
	target := 0
	mutex := sync.Mutex{} // A Mutex must not be copied after first use.

	group := sync.WaitGroup{}
	for i := 0; i < numGoroutine; i++ {
		group.Add(1)
		go func() {
			defer group.Done()
			counter(&mutex, &target)
		}()
	}
	group.Wait()

	assert.Equal(t, numGoroutine, target)
}

func counter(mutex *sync.Mutex, target *int) {
	mutex.Lock()
	*target++
	mutex.Unlock()
}
=== RUN   TestMutex
--- PASS: TestMutex (0.00s)

Mutex를 활용해 공유 자원을 대상으로 동시에 여러 Goroutine이 데이터를 쓰지 못하도록 만들어 기대한 결과가 출력될 수 있도록 만들었다.

그런데 사실 한 Goroutine에 의해 공유 자원에 쓰기 작업이 이루어지고 있지 않는 이상, 단순히 공유 자원에 접근에 데이터를 읽는 것은 동시에 이루어져도 무방하다.

RWMutex는 쓰기를 위한 Lock 획득은 오직 하나의 Goroutine만이 가능하도록 보장하면서, 읽기를 위한 Lock 획득은 동시에 여러 Goroutine이 가능하도록 설계된 Mutex이다. 여러 Goroutine에 의해 쓰기, 그리고 읽기와 쓰기가 동시에 이루어지는 상황은 방지하되, 읽기만은 여러 Goroutine이 동시에 수행할 수 있도록 허용한다.

import (
	"github.com/stretchr/testify/assert"
	"sync"
	"testing"
)

func TestMutex(t *testing.T) {
	numCounter := 10000
	numReader := 100000000
	target := 0
	mutex := sync.RWMutex{} // A Mutex must not be copied after first use.

	group := sync.WaitGroup{}
	for i := 0; i < numCounter; i++ {
		group.Add(1)
		go func() {
			defer group.Done()
			counter(&mutex, &target)
		}()
	}
	for i := 0; i < numReader; i++ {
		group.Add(1)
		go func() {
			defer group.Done()
			reader(&mutex, &target)
		}()
	}
	group.Wait()

	assert.Equal(t, numCounter, target)
}

func counter(mutex *sync.RWMutex, target *int) {
	mutex.Lock()
	defer mutex.Unlock()

	*target++
}

func reader(mutex *sync.RWMutex, target *int) int {
	mutex.RLock()
	defer mutex.RUnlock()

	return *target
}
=== RUN   TestMutex
--- PASS: TestMutex (21.51s)

위의 테스트 코드에서 reader 함수만 아래의 코드로 수정했을 때, 결과를 출력해내는데 시간이 더 소요되는 모습을 볼 수 있다.

func reader(mutex *sync.RWMutex, target *int) int {
	mutex.Lock()
	defer mutex.Unlock()

	return *target
}
=== RUN   TestMutex
--- PASS: TestMutex (31.57s)
profile
with golang

0개의 댓글