Skip to content

Latest commit

 

History

History
484 lines (352 loc) · 13.8 KB

128-139.md

File metadata and controls

484 lines (352 loc) · 13.8 KB

자원 경쟁(Data race)

경쟁 감지(Race Detection)

프로그램에 고루틴을 추가하면 복잡도가 엄청나게 올라간다. 고루틴을 언제나 상태없이(stateless) 실행할 수는 없기에 조율이 필요하다. 멀티 스레드 소프트웨어를 작성할 때는 사실상 두 가지 선택지가 있다.

  • WaitGroup에서 Add와 Done, Wait으로 제어하는 것 처럼, 공유 자원에 대한 접근 상태를 동기화하거나
  • 고루틴을 예측 가능하고, 합리적으로 실행이 되도록 만들어야 한다.

채널이 없었을 때는, 아토믹 함수나 mutex를 사용하여 앞서 언급한 두 가지 선택지를 구현하였다. 채널은 간단한 제어 방법을 제공하지만, 대부분의 경우는 아토믹 함수와 mutex를 사용하여 공유 자원에 대한 액세스 동기화를 사용하는 것이 가장 좋은 방법이다. atomic 연산은 Go에서 가장 빠른 방법이다. Go는 메모리에서 한번에 4-8 바이트씩 동기화를 하기 때문이다.

Mutex는 다음으로 빠르다. 채널은 매우 느린데, mutex일 뿐만 아니라 모든 데이터 구조와 로직이 함께 있기 때문이다. 여러개의 고루틴이 같은 자원에 접근하려할 때 자원 경쟁이 발생한다. 예를 들어, 2개의 고루틴이 int 타입의 counter라는 변수에 같은 시각에 접근하길 원하는 상황을 가정해 본다. 만약 실제로 같은 시간에 접근한다면 읽고 쓰기 위해 상호배제 할 것이다. 그렇기 때문에 공유하는 자원에 대해서 이런 접근이 필요할 때는 조정이 필요하다.

진짜 문제는 이러한 자원 경쟁이 항상 예상치 못하게 나타난다는 것이다. 예시 프로그램을 통해, 우리가 원하지 않는 자원 경쟁 상태를 만들어서 확인한다.

package main

import (
    "fmt"
    "runtime"
    "sync"
)

counter는 모든 고루틴에 의해 증가되는 변수이다.

var counter int

func main() {

사용할 고루틴의 수.�

    const grs = 2

wg는 동시성을 관리하는데 사용된다.

    var wg sync.WaitGroup
    wg.Add(grs)

2개의 고루틴을 만들어준다.

두번 반복: local counter에 읽기를 수행하고 1 씩 증가한 다음 공유 상태에 다시 쓴다. 프로그램을 실행할 때마다 출력은 4가되어야한다. 여기서 발생하는 자원 경쟁: 주어진 시간동안 두개의 고루틴은 동시에 읽고, 쓸 수 있다. 그러나 우리는 운이 좋게도 각각의 고루틴이 3번의 실행을 모두 atomic하게 실행하고있다는 것을 볼 수 있다.

만약 runtime.Goshed()라는 줄을 추가하게되면, 다른 고루틴에게 CPU를 양보하게된다. 이때, 공유 자원을 읽게되면, 강제로 context switch가 일어나게되고, 자원 경쟁이 발생할 수 있다. 그렇게 되면 다시 돌아왔을 때 4라는 결과값을 얻지 못할수도 있다.

    for i := 0; i < grs; i++ {
        go func() {
            for count := 0; count < 2; count++ {

이 때의 counter 값을 저장해 둔다.

                value := counter

다른 고루틴에게 스레드를 양보하고, 다시 대기열에 들어간다.

FOR TESTING ONLY! DO NOT USE IN PRODUCTION CODE!

                runtime.Gosched()

counter의 값을 늘린다.

                value++

값을 counter에 다시 저장한다.

                counter = value
            }
            wg.Done()
        }()
    }

고루틴이 끝날 때까지 기다린다.

    wg.Wait()
    fmt.Println("Final Counter:", counter)
}

To identify race condition : go run -race .

==================
WARNING: DATA RACE
Read at 0x000001228340 by goroutine 8:
main.main.func1()

/Users/hoanhan/work/hoanhan101/ultimate-go/go/concurrency/data_race_1.go :65 +0x47
Previous write at 0x000001228340 by goroutine 7: main.main.func1()
/Users/hoanhan/work/hoanhan101/ultimate-go/go/concurrency/data_race_1.go

:75 +0x68
Goroutine 8 (running) created at: main.main()
/Users/hoanhan/work/hoanhan101/ultimate-go/go/concurrency/data_race_1.go :62 +0xab
Goroutine 7 (finished) created at: main.main()
/Users/hoanhan/work/hoanhan101/ultimate-go/go/concurrency/data_race_1.go :62 +0xab
==================
Final Counter: 4
Found 1 data race(s)
exit status 66

아토믹 함수(Atomic Functions)

package main

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

counter는 모든 고루틴에 의해 증가되는 변수이다. 해당 변수가 int가 아닌 int64 타입이라는 것에 유의해야한다. 아토믹 함수의 경우 정확성을 요구하기 때문에 구체적인 타입을 명시해야한다.

var counter int64

func main() {

grs에 사용할 고루틴의 수를 지정.

    const grs = 2

wg는 동시성을 관리하는데 사용된다.

    var wg sync.WaitGroup
    wg.Add(grs)

2개의 고루틴들을 생성한다.

    for i := 0; i < grs; i++ {
        go func() {
            for count := 0; count < 2; count++ {

counter에 동시성에 안전하게 1을 더해준다. 동기화 보장을 원하는 대상의 주소를 첫 번째 매개변수로 하는 원지적 연산 함수를 사용한다. 같은 주소에 대해 이러한 함수들을 사용하면, 이것들은 직렬화된다. 이것이 직렬화 할 수 있는 가장 빠른 방법이다.

우리는 이 프로그램을 하루 종일 실행하더라도 매번 4라는 값을 얻을 수 있다.

                atomic.AddInt64(&counter, 1)

이 호출은 AddInt64 함수 호출이 완료됐을 때 counter가 이미 증가했으므로 큰 의미가 없다.

                runtime.Gosched()
            }
        wg.Done()
        }()
    }

고루틴이 끝날 때까지 기다린다.

    wg.Wait()

최종 값을 보여준다.

    fmt.Println("Final Counter:", counter)
}
Final Counter: 4

뮤텍스(Mutexes)

일반적으로 데이터 공유를 하기 위해 매번 4-8바이트를 할당할 만큼 메모리가 여유롭지 않다. 이럴 때 뮤텍스를 사용하면 좋다. 뮤텍스를 사용하면 모든 고루틴이 한번에 하나씩 실행할 수 있는 WaitGroup(Add, Done and Wait)과 같은 API를 사용할 수 있다.

package main

import (
    "fmt"
    "sync"
)

var (

counter는 모든 고루틴들에 의해 증가되는 변수이다.

    counter int

mutex는 코드의 임계 구역을 정의하는데 사용된다. 모든 고루틴들이 통과해야하는 방으로 mutex를 상상해보자. 그러나 한번에 하나의 고루틴만이 이동할 수 있다. 스케줄러는 누가 들어갈지, 그리고 누가 다음이될지를 정한다. 우리는 스케줄러가 무엇을 할 지 결정할 수 없다. 바라건대, 그것은 공정할 것이다. 한 고루틴이 다른 고루틴들보다 먼저 문에 도착했다고해서 먼저 끝난다는 것을 의미하지는 않는다. 여기는 예측할 수 있는것이 없다.

여기서 핵심은 들어오도록 허용된 고루틴이, 나갈 때 보고해야 한다는 것이다. 모든 고루틴들은 다른 고루틴이 들어오도록 나갈때, 잠금과 해제를 요청한다. 두 개의 다른 함수가 동일한 mutex를 사용할 수 있으므로 한번에 하나의 고루틴만 주어진 함수를 실행할 수 있다.

    mutex sync.Mutex
)
func main() {

grs에 사용할 고루틴의 수를 지정.

    const grs = 2

wg는 동시성을 관리하는데 사용된다.

    var wg sync.WaitGroup
    wg.Add(grs)

2개의 고루틴들을 생성한다.

    for i := 0; i < grs; i++ {
        go func() {
            for count := 0; count < 2; count++ {

Only allow one Goroutine through this critical section at a time. Creating these artificial curly brackets gives readability. We don't have to do this but it is highly recommended. The Lock and Unlock function must always be together in line of sight. 한번에 오직 하나의 고루틴만이 임계 구역에 들어올 수 있도록 허용된다. 이렇게 중괄호를 만들어주면 가독성이 높아진다. 이 작업을 꼭 할 필요는 없지만 적극적으로 권장한다.. 잠금과 해제 함수는 항상 같은 맥락에 있도록 해야한다.

                mutex.Lock()
                {

counter의 값을 저장한다.

                    value := counter

counter의 로컬 값을 늘린다.

                    value++

값을 다시 counter에 저장해준다.

                    counter = value
                }
                mutex.Unlock()

잠금을 해제하고, 대기중인 고루틴들이 들어올 수 있도록 허용한다.

            }

            wg.Done()
        }()
    }

고루틴이 끝날 때까지 기다린다.

    wg.Wait()
    fmt.Printf("Final Counter: %d\n", counter)
}
Final Counter: 4

읽기/쓰기 뮤텍스(Read/Write Mutex)

많은 고루틴이 읽기 원하는 공유 자원이 있다고 하자.

때때로, 하나의 고루틴이 들어와서 리소스를 바꿀 수 있다. 그렇게 되면, 모두 읽는 것을 중단해야한다. 아무런 이유 없이 소프트웨어에 대기시간을 추가하기 때문에 이러한 유형의 사나리오에서 읽기를 동기화하는 것은 의미가 없다.

package main

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

data는 공유될 slice 이다.

var (
    data []string

rwMutex는 코드의 임계 구역을 정의하는데 사용된다. 그것은 뮤텍스보다 살짝 느리지만 먼저 정확성을 최적화하고 있으므로 지금은 신경쓰지 않는다.

    rwMutex sync.RWMutex

조회하는 시간에 시도된 읽기 수를 의미한다. 여기서 int64를 보자마자 아토믹 명령어 사용에 대해 생각해야한다.

    readCount int64
)

initmain보다 먼저 호출된다.

func init() {
    rand.Seed(time.Now().UnixNano())
}

func main() {

wg는 동시성을 관리하는데 사용된다.

    var wg sync.WaitGroup
    wg.Add(1)

10개의 서로 다른 쓰기를 수행하는 쓰기용 고루틴을 만든다.

    go func() {
        for i := 0; i < 10; i++ {
            time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
            writer(i)
        }
        wg.Done()
    }()

영원히 실행되는 8개의 읽기용 고루틴을 만든다.

    for i := 0; i < 8; i ++ {
        go func(i int) {
            for {
                reader(i)
            }
        }(i)
    }

쓰기 고루틴이 끝날 때까지 기다린다.

    wg.Wait()
    fmt.Println("Program Complete")
}

쓰기용 고루틴은 임의의 간격으로 슬라이스에 새 문자열을 추가한다.

func writer(i int) {

한번에 오직 하나의 고루틴만이 슬라이스에 읽기/쓰기를 하도록 허용된다.

    rwMutex.Lock()
    {

현재 readCount를 캡쳐한다. 이 호출 없이 수행할 수 있지만 안전하게 처리하는 것이 좋다. 다른 고루틴이 읽기를 수행하지 않는 것을 보장해야 한다. 이 코드가 실행될 때 rc의 값은 항상 0 이어야한다.

        rc := atomic.LoadInt64(&readCount)

전체 잠금이 있으므로 작업을 수행한다.

        fmt.Printf("****> : Performing Write : RCount[%d]\n", rc)
        data = append(data, fmt.Sprintf("String: %d", i))
    }
    rwMutex.Unlock()
}

reader가 수행되고 데이터 슬라이스를 반복한다.

func reader(id int) {

모든 고루틴은 쓰기 작업이 일어나지 않을 때 읽을 수 있다. RLock에는 그에 해당하는 RUnlock이 있다.

    rwMutex.RLock()
    {

readCount를 1씩 증가시킨다.

        rc := atomic.AddInt64(&readCount, 1)

읽기 작업을 수행하고 값을 표시한다.

        time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond)
        fmt.Printf("%d : Performing Read : Length[%d] RCount[%d]\n", id, len(data), rc)

readCount를 1씩 감소시킨다.

        atomic.AddInt64(&readCount, -1)
    }
    rwMutex.RUnlock()
}

출력은 이와 유사하게 잠긴다.

0 : Performing Read : Length[0] RCount[1]
4 : Performing Read : Length[0] RCount[5]
5 : Performing Read : Length[0] RCount[6]
7 : Performing Read : Length[0] RCount[7]
3 : Performing Read : Length[0] RCount[4]
6 : Performing Read : Length[0] RCount[8]
4 : Performing Read : Length[0] RCount[8]
1 : Performing Read : Length[0] RCount[2]
2 : Performing Read : Length[0] RCount[3]
5 : Performing Read : Length[0] RCount[8]
0 : Performing Read : Length[0] RCount[8]
7 : Performing Read : Length[0] RCount[8]
7 : Performing Read : Length[0] RCount[8]
2 : Performing Read : Length[0] RCount[8]
...
1 : Performing Read : Length[10] RCount[8]
5 : Performing Read : Length[10] RCount[8]
3 : Performing Read : Length[10] RCount[8]
4 : Performing Read : Length[10] RCount[8]
6 : Performing Read : Length[10] RCount[8]
7 : Performing Read : Length[10] RCount[8]
2 : Performing Read : Length[10] RCount[8]
2 : Performing Read : Length[10] RCount[8]

Lesson:

아토믹 함수와 뮤텍스는 사용자의 소프트웨어에 대기시간을 만든다. 대기시간은 여러 고루틴 사이에 자원 접근에 대한 조율이 필요할 때 유용하다. Read/Write 뮤텍스는 대기시간을 줄이는 데 유용하다.

뮤텍스를 사용하는 경우, 잠금 이후 최대한 빨리 잠금을 해제해야 한다. 다른 불필요한 행위는 하지 않는 것이 좋다. 때로는 공유 자원을 읽기를 위해 로컬 변수만 사용하는 것으로도 충분하다. 뮤텍스를 적게 사용할수록 좋다. 이를 통해 대기시간을 최소한으로 줄일 수 있다.