더북(TheBook)

동시성을 처리할 때 sync 패키지의 저수준 기능(뮤텍스, Lock 등)이 아니라, 채널을 사용한다 해도 교착상태(deadlock) 위험은 여전히 남아있다. 예를 들어 서로 데이터를 주고받는 고루틴이 있다고 해보자. 이를테면 고루틴1은 고루틴2로부터 데이터를 받아 처리한 후 결과를 고루틴2로 보내고, 고루틴2도 데이터를 받아 처리한 다음 다시 고루틴1로 전달하는 상황이다. 여기서 두 고루틴은 교착상태에 빠질 수 있다.

Go는 교착상태와 경쟁상태(race condition)를 테스트하기 위해 Race Detector(https://golang.org/doc/articles/race_detector.html)를 제공한다. -race 플래그와 함께 Go 프로그램을 실행하면 경쟁상태를 확인해준다.

명령 프롬프트

$ go test -race mypkg

$ go run -race mysrc.go

$ go build -race mypkg

$ go install -race mypkg

Note 채널을 사용할 때 주의할 점

함수와 마찬가지로 채널도 값에 의한 호출 방식으로 값을 전달한다. 즉, 실제 값이 복사되어 전달되므로 bool, int, float64 등의 값을 전달하는 것은 안전하다. Go에서는 문자열과 배열도 변하지 않는 값이므로 채널의 값으로 사용해도 안전하다.

하지만 포인터 변수나 참조 값(슬라이스, 맵)을 채널로 전달할 때는 주소 값이 전달되므로 값을 보내는 고루틴과 값을 받는 고루틴에서 값을 동시에 수정하면 예상치 못한 결과가 발생할 수 있다. 그래서 포인터나 참조 값을 채널로 전달할 때는 여러 고루틴에서 값을 동시에 수정하지 않게 해야 한다. 가장 간단한 방법은 여러 고루틴에서 참조 값에 동시에 접근할 수 없게 뮤텍스로 제한하는 것이다.

참조 값을 동시에 변경할 위험이 있는 고루틴에는 참조 값을 직접 전달하는 것이 아니라, 인터페이스를 전달하는 것도 좋은 방법이다. 참조 값에 대한 읽기 전용 인터페이스를 만들고, 채널로 읽기 전용 인터페이스를 전달하면 안전하게 참조 값을 사용할 수 있다.

다음 코드에서는 Get() 메서드를 가진 mapGetter 인터페이스를 정의하고, stringMap 타입에도 Get() 메서드를 추가해준다.

type mapGetter interface {
    Get(s string) interface{}
}
 
type stringMap map[string]interface{}
 
func (m stringMap) Get(s string) interface{} { return m[s] }

stringMap의 값을 변경할 필요가 없는 고루틴에는 stringMap을 직접 전달하지 않고 mapGetter 인터페이스를 채널로 전달한다. 그러면 개발자가 실수로 stringMap의 값을 변경하는 것을 막을 수 있다.

func process(g <-chan mapGetter) {
    ...
    map := <-g
    value := map.Get("key")
    ...
}
신간 소식 구독하기
뉴스레터에 가입하시고 이메일로 신간 소식을 받아 보세요.