Go 동시성 실전 — 채널이냐 뮤텍스냐, 언제 무엇을

English version

TECH INSIGHT / EXPLAINED

동시성 도구는 소유권으로 고릅니다. 짧은 공유 상태 갱신에는 mutex, 작업이나 이벤트를 다른 고루틴에 넘길 때는 channel이 잘 맞습니다. 한 봇에서 둘을 함께 써도 되며 종료·복사·차단 동작이 분명해야 합니다.

동료 세 명이 Go 공유 상태와 작업 전달의 소유권 주제를 그림으로 설명하는 모습
이해를 돕기 위한 AI 생성 설명 일러스트. 실제 운영 화면이나 실적 자료가 아닙니다.

잠금만으로 슬라이스 별칭 문제가 사라지지 않는다

호출자의 슬라이스를 보호된 필드에 그대로 대입하면 호출자가 같은 내부 배열을 잠금 없이 바꿀 수 있습니다. 저장 전에 입력을 복사하세요. 아래 통합용 코드는 import "sync"가 필요하며, Set이 복사하는 동안 호출자가 입력을 동시에 바꾸면 안 됩니다. RPCNode가 값 필드만 가지므로 여기서는 한 항목을 값으로 반환하면 충분합니다. 내부에 가변 데이터를 넣으면 별도 복사·소유권 규칙이 필요합니다.

type RPCNode struct { URL string; LatencyMS int64 }
type Nodes struct { mu sync.RWMutex; items []RPCNode }

func (s *Nodes) Set(nodes []RPCNode) {
    copyOfNodes := append([]RPCNode(nil), nodes...)
    s.mu.Lock()
    s.items = copyOfNodes
    s.mu.Unlock()
}

func (s *Nodes) First() (RPCNode, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    if len(s.items) == 0 { return RPCNode{}, false }
    return s.items[0], true // RPCNode has value fields only
}

RPC나 디스크 작업 동안 잠금을 잡지 않습니다. 필요한 상태를 복사해 잠금을 풀고 작업한 뒤, 결과가 여전히 현재 설정·세대에 속하는지 확인해 적용합니다. RWMutex가 Mutex보다 항상 빠르지는 않으므로 실제 작업 부하로 측정합니다.

워커 풀은 전달 단계마다 취소가 필요하다

실행 가능한 다음 예제는 작업자 수를 제한하고 작업 제출과 결과 전달 모두 취소할 수 있게 하며, 송신이 끝난 뒤 채널을 닫습니다. 외부 서비스 없이 수명 관리를 보여주려고 작은 정수의 제곱을 계산합니다. 실제 견적 조회 함수도 context와 전송 제한 시간을 지원해야 합니다. 바깥쪽 select가 협조하지 않는 함수를 대신 중단하지는 못합니다.

package main

import (
    "context"
    "fmt"
    "sort"
    "sync"
)

func squares(ctx context.Context, values []int, workers int) ([]int, error) {
    if workers < 1 || workers > 32 { return nil, fmt.Errorf("workers must be 1..32") }
    if err := ctx.Err(); err != nil { return nil, err }
    jobs, results := make(chan int), make(chan int)
    var wg sync.WaitGroup
    wg.Add(workers)
    for i := 0; i < workers; i++ {
        go func() {
            defer wg.Done()
            for {
                select {
                case <-ctx.Done(): return
                case n, ok := <-jobs:
                    if !ok { return }
                    // This example uses small integers, no blocking I/O.
                    result := n*n
                    select {
                    case <-ctx.Done(): return
                    case results <- result:
                    }
                }
            }
        }()
    }
    go func() {
        defer close(jobs) // sole producer owns this close
        for _, n := range values {
            select {
            case <-ctx.Done(): return
            case jobs <- n:
            }
        }
    }()
    go func() { wg.Wait(); close(results) }()
    var out []int
    for result := range results { out = append(out, result) }
    if err := ctx.Err(); err != nil { return nil, err }
    sort.Ints(out) // worker completion order is not input order
    return out, nil
}

func main() {
    out, err := squares(context.Background(), []int{3, 2}, 2)
    fmt.Println(out, err)
}

출력은 [4 9] <nil>입니다. 작업자의 완료 순서는 달라질 수 있어 표시 전에 정렬합니다. 호출 중 입력 슬라이스는 변경하지 않습니다. 예제는 취소 시 일부 결과를 버리며 실제 인터페이스는 부분 결과를 사용할지 명시해야 합니다.

정상 결과 외에 종료를 시험한다

작업자 0명·과다 인원, 빈 입력, 큰 배치 중 취소, 수신자 조기 중단을 시험합니다. 스트리밍 API에서 수신을 멈추면 취소도 전달해 송신자가 끝나게 해야 합니다. 여러 select 분기가 준비되면 취소 분기에 특별한 우선순위가 있는 것은 아니며 Go가 준비된 통신 중 선택합니다. 지원되는 도구 체인에서 go test -race로 실행된 경로의 데이터 경쟁을 검사하고 제한 시간·수명 시험으로 멈춤도 확인합니다. race detector가 교착이나 실행하지 않은 경로의 경쟁이 없다고 증명하지는 않습니다.

공식 문서와 이어 읽기

내용·예제 검토: 2026-09-29

댓글