동기화 객체 사용하기

이재홍 http://www.pyrasis.com 2014.12.17 ~ 2015.02.07

Go 언어에서는 채널 이외에도 고루틴의 실행 흐름을 제어하는 동기화 객체를 제공합니다.

대표적인 동기화(synchronization) 객체는 다음과 같습니다.

  • Mutex: 뮤텍스입니다. 상호 배제(mutual exclusion)라고도 하며 여러 스레드(고루틴)에서 공유되는 데이터를 보호할 때 주로 사용합니다.
  • RWMutex: 읽기/쓰기 뮤텍스입니다. 읽기와 쓰기 동작을 나누어서 잠금(락)을 걸 수 있습니다.
  • Cond: 조건 변수(condition variable)입니다. 대기하고 있는 하나의 객체를 깨울 수도 있고 여러 개를 동시에 깨울 수도 있습니다.
  • Once: 특정 함수를 딱 한 번만 실행할 때 사용합니다.
  • Pool: 멀티 스레드(고루틴)에서 사용할 수 있는 객체 풀입니다. 자주 사용하는 객체를 풀에 보관했다가 다시 사용합니다.
  • WaitGroup: 고루틴이 모두 끝날 때까지 기다리는 기능입니다.
  • Atomic: 원자적 연산이라고도 하며 더 이상 쪼갤 수 없는 연산이라는 뜻입니다. 멀티 스레드(고루틴), 멀티코어 환경에서 안전하게 값을 연산하는 기능입니다.

뮤텍스 사용하기

뮤텍스는 여러 고루틴이 공유하는 데이터를 보호할 때 사용하며 sync 패키지에서 제공하는 뮤텍스 구조체와 함수는 다음과 같습니다.

  • sync.Mutex
  • func (m *Mutex) Lock(): 뮤텍스 잠금
  • func (m *Mutex) Unlock(): 뮤텍스 잠금 해제

다음은 고루틴 두 개에서 각각 1,000번씩 슬라이스에 값을 추가합니다.

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	runtime.GOMAXPROCS(runtime.NumCPU()) // 모든 CPU 사용

	var data = []int{} // int형 슬라이스 생성

	go func() {                             // 고루틴에서
		for i := 0; i < 1000; i++ {     // 1000번 반복하면서
			data = append(data, 1)  // data 슬라이스에 1을 추가

			runtime.Gosched()       // 다른 고루틴이 CPU를 사용할 수 있도록 양보
		}
	}()

	go func() {                             // 고루틴에서
		for i := 0; i < 1000; i++ {     // 1000번 반복하면서
			data = append(data, 1)  // data 슬라이스에 1을 추가

			runtime.Gosched()       // 다른 고루틴이 CPU를 사용할 수 있도록 양보
		}
	}()

	time.Sleep(2 * time.Second)      // 2초 대기

	fmt.Println(len(data))           // data 슬라이스의 길이 출력
}

출력 결과

1883 (매번 달라질 수 있음)

실행을 해보면 대략 1800~1990 사이의 값이 나옵니다. data 슬라이스에 1을 2,000번 추가했으므로 data의 길이가 2000이 되어야 하는데 그렇지가 않습니다. 두 고루틴이 경합을 벌이면서 동시에 data에 접근했기 때문에 append 함수가 정확하게 처리되지 않은 상황입니다. 이러한 상황을 경쟁 조건(Race condition)이라고 합니다.

runtime.Gosched 함수는 다른 고루틴이 CPU를 사용할 수 있도록 양보(yield)합니다. 지금까지 time.Sleep 함수를 사용했지만 runtime.Gosched 함수가 좀 더 명확합니다.

경쟁 조건과 멀티코어
CPU의 코어가 한 개인 컴퓨터에서 실행했다면 경쟁 조건(race condition) 상황이 발생하지 않고, 정확하게 2,000이 출력될 수도 있습니다(runtime.GOMAXPROCS(1)로 실행하면 CPU의 코어를 한 개만 사용). 예제 코드는 반복 횟수가 적어서 경쟁 조건이 거의 발생하지 않지만 반복 횟수가 많아지면 CPU 코어가 한 개라도 경쟁 조건이 발생합니다.

요즘 나오는 CPU는 코어가 여러 개이거나 코어가 한 개라도 하이퍼스레딩 기술을 제공하므로 논리적으로는 CPU가 여러 개인 상태입니다. 자신의 CPU 코어 또는 하이퍼스레딩 개수를 알고 싶으면 다음과 같이 runtime.NumCPU 함수를 사용합니다.

fmt.Println(runtime.NumCPU())

CPU의 코어가 여러 개인 컴퓨터에서는 여러 CPU 코어에서 동시에 공유 데이터에 접근할 수 있으므로 경쟁 조건이 발생합니다.

이제 data 슬라이스를 뮤텍스로 보호해보겠습니다.

package main

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

func main() {
	runtime.GOMAXPROCS(runtime.NumCPU()) // 모든 CPU 사용

	var data = []int{}
	var mutex = new(sync.Mutex)

	go func() {                             // 고루틴에서
		for i := 0; i < 1000; i++ {     // 1000번 반복하면서
			mutex.Lock()            // 뮤텍스 잠금, data 슬라이스 보호 시작
			data = append(data, 1)  // data 슬라이스에 1을 추가
			mutex.Unlock()          // 뮤텍스 잠금 해제, data 슬라이스 보호 종료

			runtime.Gosched()       // 다른 고루틴이 CPU를 사용할 수 있도록 양보
		}
	}()

	go func() {                             // 고루틴에서
		for i := 0; i < 1000; i++ {     // 1000번 반복하면서
			mutex.Lock()            // 뮤텍스 잠금, data 슬라이스 보호 시작
			data = append(data, 1)  // data 슬라이스에 1을 추가
			mutex.Unlock()          // 뮤텍스 잠금 해제, data 슬라이스 보호 종료

			runtime.Gosched()       // 다른 고루틴이 CPU를 사용할 수 있도록 양보
		}
	}()

	time.Sleep(2 * time.Second) // 2초 대기

	fmt.Println(len(data)) // data 슬라이스의 길이 출력
}

뮤텍스는 sync.Mutex를 할당한 뒤에 고루틴에서 Lock, Unlock 함수로 사용합니다. 보호를 시작할 부분에서 Lock 함수를 사용하고, 보호를 끝낼 부분에서 Unlock 함수를 사용합니다. Lock, Unlock 함수는 반드시 짝을 맞추어주어야 하며 짝이 맞지 않으면 데드락(deadlock, 교착 상태)이 발생하므로 주의합니다.

여기서는 data 슬라이스를 보호할 것이므로 두 고루틴 모두 data = append(data, 1) 부분 위 아래로 Lock, Unlock 함수를 사용합니다. 이제 실행을 해보면 정확히 2000이 출력됩니다.

실행 결과

2000

저작권 안내

이 웹사이트에 게시된 모든 글의 무단 복제 및 도용을 금지합니다.
  • 블로그, 게시판 등에 퍼가는 것을 금지합니다.
  • 비공개 포스트에 퍼가는 것을 금지합니다.
  • 글 내용, 그림을 발췌 및 요약하는 것을 금지합니다.
  • 링크 및 SNS 공유는 허용합니다.

Published

01 June 2015