Skip to content

context

context 는 컨텍스트로 번역되며, 설계初衷은 여러 코루틴, 특히 부모 - 자식 코루틴 간에 신호와 간단한 데이터를 전달하는 데 있습니다. 이는 일반적으로 HTTP 요청 처리, 작업 스케줄링, 데이터베이스 쿼리 등의 시나리오에서 사용되며, 특히 마이크로서비스 아키텍처에서 gRPC 는 context 를 통해 프로세스 간 및 네트워크를 넘어 메타데이터 전달, 링크 제어 등의 작업을 수행합니다.

go
package main

import (
    "context"
    "fmt"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
    defer stop()
    for {
       select {
       case <-ctx.Done():
          fmt.Println("terminate")
          return
       default:
       }
       fmt.Println("running")
       time.Sleep(100 * time.Millisecond)
    }
}

위 사례에서 context 를 통해 프로세스 신호를 전달하며, 신호를 수신하면 프로그램이 자체 종료됩니다. 이 역시 context 의 응용 시나리오 중 하나입니다.

구조

context.Context 는 구체적인 구현이 아닌 일련의 메서드를 정의한 인터페이스입니다.

go
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error

	Value(key any) any
}
  • Deadline(), 마감 날짜와 종료 여부를 나타내는 불리언 값을 반환합니다.
  • Done(), 종료 알림을 위한 채널을 반환합니다.
  • Err(), 컨텍스트가 종료된 이유를 반환합니다.
  • Value(), 키에 따라 지정된 값을 반환합니다.

표준 라이브러리에는 다음 몇 가지 사용 가능한 컨텍스트가 있습니다.

  • Background, 빈 컨텍스트로, 일반적으로 컨텍스트의 루트 노드로 사용됩니다.
  • WithCancel, WithCancelCause, 취소 가능한 컨텍스트.
  • WithDeadline, WithDeadlineCause, 마감 시간이 있는 컨텍스트.
  • WithTimeout, WithTimeoutCause, 초과 시간이 있는 컨텍스트.
  • WithValue, 값을 전달할 수 있는 컨텍스트.

구현에 있어서는 주로 다음이 있습니다.

  • timerCtx
  • cancelCtx
  • emptyCtx

따라서 핵심 기능은 네 가지입니다.

  • 취소
  • 마감
  • 값 전달
  • 전파

이 몇 가지를 이해하면 기본적으로 context 의 작동 원리를 알 수 있습니다.

취소

go
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
	cause    error                 // set to non-nil by the first cancel call
}


func withCancel(parent Context) *cancelCtx {
    if parent == nil {
       panic("cannot create context from nil parent")
    }
    c := &cancelCtx{}
    c.propagateCancel(parent, c)
    return c
}

cancelCtx 의 핵심은 propagateCancel 메서드에 있으며, 이는 취소 가능한 행위를 부모 - 자식 컨텍스트에 전파하는 역할을 담당합니다.

go
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	...
}
  1. 먼저 부모 context 가 취소 가능한지 확인하며, 불가능하면 바로 반환합니다.

    go
    done := parent.Done()
    if done == nil {
    	return // parent is never canceled
    }
  2. 부모 context 가 이미 취소되었는지 확인하며, 취소되었다면 모든 자식 context 를 취소합니다.

    go
    select {
    case <-done:
    	// parent is already canceled
    	child.cancel(false, parent.Err(), Cause(parent))
    	return
    default:
    }
  3. 부모 context 를 cancelCtx 유형으로 변환하려고 시도하며, 성공하면 현재 context 를 부모 context 의 children 에 추가합니다.

    go
    if p, ok := parentCancelCtx(parent); ok {
    	// parent is a *cancelCtx, or derives from one.
    	p.mu.Lock()
    	if p.err != nil {
    		// parent has already been canceled
    		child.cancel(false, p.err, p.cause)
    	} else {
    		if p.children == nil {
    			p.children = make(map[canceler]struct{})
    		}
    		p.children[child] = struct{}{}
    	}
    	p.mu.Unlock()
    	return
    }
  4. afterFuncer 유형으로 변환하려고 시도하며, 성공하면 현재 context 취소 메서드를 부모 context 의 AfterFunc 에 등록합니다.

    go
    if a, ok := parent.(afterFuncer); ok {
    	// parent implements an AfterFunc method.
    	c.mu.Lock()
    	stop := a.AfterFunc(func() {
    		child.cancel(false, parent.Err(), Cause(parent))
    	})
    	c.Context = stopCtx{
    		Context: parent,
    		stop:    stop,
    	}
    	c.mu.Unlock()
    	return
    }
  5. 그래도 안 되면 별도의 코루틴을 열어 Done 채널을 모니터링하며, 신호를 수신하면 자식 context 를 취소합니다.

    go
    go func() {
        select {
        case <-parent.Done():
           child.cancel(false, parent.Err(), Cause(parent))
        case <-child.Done():
        }
    }()

그런 다음 cancelCtx.cancel 메서드가 최종적으로 자식 context 취소를 담당합니다.

go
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	...
}

플로우는 다음과 같습니다.

  1. 현재 context 가 이미 취소되었는지 확인합니다.

    go
    if err == nil {
    	panic("context: internal error: missing cancel error")
    }
    if cause == nil {
    	cause = err
    }
    c.mu.Lock()
    if c.err != nil {
    	c.mu.Unlock()
    	return // already canceled
    }
  2. done 채널을 닫아 종료 알림을 보냅니다.

    go
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	c.done.Store(closedchan)
    } else {
    	close(d)
    }
  3. 자식 context 를 순회하며 알림을 보냅니다.

    go
    for child := range c.children {
    	child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()
  4. 마지막으로 전달된 매개변수에 따라 부모 context 에서 제거할지 판단합니다.

    if removeFromParent {
    	removeChild(c.Context, c)
    }

마감

WithTimeoutWithDeadline 은 모두 마감 시간이 있는 컨텍스트로, 둘은 동일한 유형이며 사용 의미만 다를 뿐이며, 모두 cancelCtx 를 기반으로 합니다.

go
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

WithDeadlineCause 는 마감 시간이 있는 컨텍스트를 생성합니다.

go
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
	...
}

플로우는 다음과 같습니다.

  1. 마감 날짜를 확인하며, 부모 마감 날짜가 현재 마감 날짜보다 빠르면 부모 context 가 현재 context 보다 먼저 취소되므로 cancelCtx 유형의 컨텍스트를 직접 생성합니다.

    go
    if parent == nil {
    	panic("cannot create context from nil parent")
    }
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
    	// The current deadline is already sooner than the new one.
    	return WithCancel(parent)
    }
  2. timeCtx 를 구축하고 자식 context 에 전파합니다.

    go
    c := &timerCtx{
    	deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
  3. 현재 마감 시간을 계산하며, 이미 마감되었으면 바로 취소합니다.

    go
    dur := time.Until(d)
    if dur <= 0 {
    	c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
    	return c, func() { c.cancel(false, Canceled, nil) }
    }
  4. 마감되지 않았다면 time.AfterFunc 를 사용하여 마감 시간에 현재 context 를 취소하도록 설정합니다.

    go
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
    	c.timer = time.AfterFunc(dur, func() {
    		c.cancel(true, DeadlineExceeded, cause)
    	})
    }
    return c, func() { c.cancel(true, Canceled, nil) }

timerCtx 의 경우 컨텍스트 취소 메서드는 단순히 timer 를 중지하고 자식 context 도 함께 중지합니다.

go
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
	c.cancelCtx.cancel(false, err, cause)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

값 전달

valueCtx 는 컨텍스트에서 값을 전달하고 가져올 수 있습니다.

go
type valueCtx struct {
    Context
    key, val any
}

func WithValue(parent Context, key, val any) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

핵심은 value 함수에 있으며, 다른 context 구체적 구현도 이 함수를 통해 값을 가져옵니다. 예를 들어

go
func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, key)
}

func (c *cancelCtx) Value(key any) any {
	if key == &cancelCtxKey {
		return c
	}
	return value(c.Context, key)
}

value 함수는 큰 for 루프로, 지속적으로 상향 재귀하며 지정된 키의 값을 찾습니다.

go
func value(c Context, key any) any {
	for {
		switch ctx := c.(type) {
		case *valueCtx:
			if key == ctx.key {
				return ctx.val
			}
			c = ctx.Context
		case *cancelCtx:
			if key == &cancelCtxKey {
				return c
			}
			c = ctx.Context
		case withoutCancelCtx:
			if key == &cancelCtxKey {
				// This implements Cause(ctx) == nil
				// when ctx is created using WithoutCancel.
				return nil
			}
			c = ctx.c
		case *timerCtx:
			if key == &cancelCtxKey {
				return &ctx.cancelCtx
			}
			c = ctx.Context
		case backgroundCtx, todoCtx:
			return nil
		default:
			return c.Value(key)
		}
	}
}

다른 Context 유형의 처리:

  • *valueCtx: key 가 현재 Context 의 key 와 일치하면 key 에 해당하는 val 을 반환합니다.
  • *cancelCtx: keycancelCtxKey 와 일치하면 현재 cancelCtx 를 반환하며, 이는 자신 자신을 반환함을 의미합니다.
  • withoutCancelCtx: 취소 기능이 없는 Context 를 나타내며, keycancelCtxKey 와 일치하면 nil 을 반환합니다.
  • *timerCtx: keycancelCtxKey 와 일치하면 연관된 cancelCtx 를 반환합니다.
  • backgroundCtxtodoCtx: 일반적으로 추가 값을携带하지 않는 특수 유형의 Context 로, 이 두 유형을 만나면 바로 nil 을 반환합니다.
  • 알 수 없는 유형인 경우 Value 메서드를 계속 호출하여 찾습니다.

요약

이러한 context 의 핵심은 cancelCtx 로, 이를 통해 취소 신호를 전파하며, 다른 context 유형 및 일부 서드파티 context 유형도 이 위에 층층이 쌓아 다양한 기능을 구현합니다.

Golang by www.golangdev.cn edit