Go/Golang - Concurrent Programming
Go Routine
GoRoutine
은 Go언어에서 관리하는 lightweight thread
이다. 그리고 이런 여러 GoRoutine
을 가지는 프로그램을 동시성 프로그래밍(Concurrent Programming)
이라고 한다. 아래와 같은 방법으로 GoRoutine을 선언하게 되면 생성되는 GoRoutine은 3개다. PrintHangul()
, PrintNumbers()
그리고 main()
이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // 예시
// go function_call()
func PrintHangul() {
hanguls := []rune{ '가', '나', '다', '라', '마', '바', '사' }
for _, v := range hanguls {
time.Sleep(300 * time.Millisecond)
fmt.Printf("%c ", v)
}
}
func PrintNumbers() {
for i := 1; i <= 5; i++ {
time.Sleep(400 * time.Millisecond)
fmt.Printf("%d ", i)
}
}
func main() {
go PrintHangul()
go PrintNumbers()
// 이 main() 이 종료되면 프로그램이 종료다!
// 다른 Go Routine을 기다리지 않는다!
time.Sleep(3 * time.Second)
}
|
Sub GoRoutine
Go에서는 기존 OS thread 에서 발생되는 Context-Switching
비용이 발생하지 않아 동시성 프로그래밍에 장점을 가진다. CPU 코어당 OS thread 하나만을 할당하기 때문에, thread Context (thread instruction pointer, stack memory)
를 저장해 둘 필요가 없기 때문이다. CPU 코어수를 초과해 들어오는 thread(GoRoutine)는 대기상태
로 들어간다. 이 GoRoutine들의 종료를 보증하는 방법으로 그저 Sleep()은 부족하다. 따라서 GoRoutine이 종료될 때 까지 대기하기 위해 WaitGroup 객체를 사용헌다!
1
2
3
4
5
| var wg sync.WaitGroup
wg.Add(3) // 작업 개수 설정
wg.Done() // 작업이 완료될 때마다 호출
wg.Wait() // 모든 작업이 롼료될 때까지 대기한다.
|
실제 사용방법은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| var wg sync.WaitGroup
func SumAtoB(a, b int) {
sum := 0
for i := a; i <= b; i++ {
sum += i
}
fmt.Printf("%d부터 %d까지 합계는 %d입니다.\n", a, b, sum)
wg.Done() // 함수가 완료되고 나서는 남은작업 개수 -1개
}
func main() {
wg.Add(10)
for i := 0; i < 10; i++ {
go SumAtoB(1, 1000000)
}
wg.Wait() // 남은 함수 개수가 0이 되는 순간 Wait() 메서드가 종료
fmt.Println("모든 계산이 완료되었습니다.")
}
|
동시성 프로그래밍의 문제
동시성 프로그램의 문제는 같은 동일 자원에 여러 고루틴이 접근할 때 발생한다. 이를 막기 위한 방법으로 1) Mutex(뮤텍스, Mutual Exclusion) 과 2) 영역분할, 3) 역할 분할이 있다.
Mutex
뮤텍스는 Lock() 메소드를 통해 뮤텍스를 획득하며, 한번 획득한 뮤텍스는 반드시 Unlock() 메소드를 호출해 반납해야 한다. 이런 방식으로 동일 자원에 접근하는 문제는 해결했지만 동시성 프로그래밍으로 얻는 성능향상을 잃었다. 오직 하나의 고루틴만 공유자원에 접근 가능하다면 한번에 하나의 고루틴이 돌아가는 것과 동일하기 때문이다.
Mutex and DeadLock
추가로 DeadLock이 발생할 수 있다. 고루틴들중 누구도 원하는 만큼의 뮤텍스를 획득하지 못해 종료되지 않으면(대기상태에 있게 되면) 무한히 그 상태가 유지될 수 있기 때문이다. 아래 코드를 실행하면 이를 구현할 수 있다. 영역을 나누거나 역할을 나누는 특별한 방법을 사용하지 않는 이상은 Mutex도 여전히 유용하고 손쉬운 방법이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| var wg sync.WaitGroup
func diningProblem(name string, first, second *sync.Mutex, firstName, secondName string) {
for i := 0; i < 100; i++ {
fmt.Printf("%s 밥을 먹으려 합니다.\n", name)
first.Lock()
fmt.Printf("%s %s 획득.\n", name, firstName)
second.Lock()
fmt.Printf("%s %s 획득.\n", name, secondName)
fmt.Printf("%s 밥을 먹습니다.\n", name)
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
second.Unlock()
first.Unlock() // 뮤텍스 반납
}
wg.Done()
}
func main() {
rand.Seed(time.Now().UnixNano())
wg.Add(2)
fork := &sync.Mutex{}
spoon := &sync.Mutex{}
go diningProblem("A", fork, spoon, "포크", "수저")
go diningProblem("B", spoon, fork, "포크", "수저")
wg.Wait()
}
|
영역 분할과 역할 분할
따라서 이런 상황을 막기 위해, 자원의 영역을 나누어 고루틴들에게 골고루 나눠주는 방법이 있다.(역할을 나누는 법은 다음장이다)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| type Job interface {
Do()
}
type SquareJob struct {
index int
}
func (j *SquareJob) Do() {
fmt.Printf("%d 작업 시작\n", j.index)
time.Sleep(1 * time.Second)
fmt.Printf("%d 작업 완료 - 결과: %d\n", j.index, j.index*j.index)
}
func main() {
var jobList [10]Job
for i := 0; i < 10; i++ {
jobList[i] = &SquareJob{i}
}
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
job := jobList[i]
go func() {
job.Do()
wg.Done()
}()
}
wg.Wait()
}
|
Questions
Q1. Go 가 동시성 프로그래밍에 유리한 이유는 무엇인가?
1
2
3
| Go에서는 기존 OS thread 에서 발생되는 `Context-Switching` 비용이 발생하지 않아 동시성 프로그래밍에 장점을 가진다.
CPU 코어당 OS thread 하나만을 할당하기 때문에, `thread Context (thread instruction pointer, stack memory)`
를 저장해 둘 필요가 없기 때문이다.
|
Q2. Go-Routine에서 WaitGroup의 3가지 함수의 역할을 말하라
1
2
3
| wg.Add(3) // 작업 개수 설정
wg.Done() // 작업이 완료될 때마다 호출
wg.Wait() // 모든 작업이 롼료될 때까지 대기한다.
|
Q3. Mutex의 사용방법을 말하라. Mutex의 단점은 무엇인가?
1
2
3
| 1) 뮤텍스는 Lock() 메소드를 통해 뮤텍스를 획득하며, 한번 획득한 뮤텍스는 반드시 Unlock() 메소드를 호출해 반납해야 한다.
2) 동일 자원에 접근하는 문제는 해결했지만 동시성 프로그래밍으로 얻는 성능향상을 잃었다. 오직 하나의 고루틴만 공유자원에
접근 가능하다면 한번에 하나의 고루틴이 돌아가는 것과 동일하기 때문이다.
|