golang 비동기. context 동작 원리
해당 아티클은 다음 수준의 지식을 요구합니다.
- 고루틴, 채널, select문과 같은 golang 비동기 기본
golang 비동기. 대기행렬이론과 이벤트루프 안전성 아티클을 읽고 오시길 권장합니다. 물론 읽지 않아도 어려운 점은 없습니다.
지금까지 golang의 기본적인 비동기를 사용하기 위한 패키지인 sync, atomic과 같은 패키지를 학습하고 eventloop를 구현했습니다. 또 eventloop를 안전하게 하기 위해 대기행렬이론의 접근을 활용했습니다. 해당 분석을 통해 eventloop에서 event를 안전하게 디스패칭하기 위해 여러 안전성 패턴이 필요함을 이해했습니다. context 패키지는 golang에서 여러 안전성 패턴을 구현하기 위한 패키지입니다. http, grpc, message queue, 데이터베이스 작업 등에서 작업 간 상태를 전달하고 실행 중인 고루틴을 외부에서 취소할 수 있는 기능을 제공합니다. 또한 고루틴에서 다른 고루틴으로 정보를 전달하여 telemetry 기능을 구현하는 방식으로도 활용합니다. 이번에는 context에 대하여 이해해봅시다.
context 패키지 소개
golang 사용자에게 많이 익숙한 context는 보통 두 가지 경우에 사용합니다.
- 외부 라이브러리에 존재하는 함수나 메서드를 호출할 때 라이브러리 밖에서 함수의 수명을 제어하기 위해 사용합니다.
- 여러 함수나 메서드가 깊이 호출되거나 데이터가 비동기로 전달되는 상황에서 호출된 함수의 루트로부터 임의의 데이터를 전파하는 경우에 사용합니다. 해당 예시로 opentelemetry를 이용한 trace정보 전파가 있습니다.
1번의 경우 대체로 context패키지의 다음 함수를 이용하여 구현합니다:
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L235
// 입력으로 부모 context를 받습니다.
// 출력으로 자식 context와 자식 context를 취소할 수 있는 함수 CancelFunc을 받습니다.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L689
// 입력으로 부모 context와 실행 시간을 받습니다.
// 출력으로 자식 context와 자식 context를 취소할 수 있는 함수 CancelFunc을 받습니다.
// 자식 context는 timeout이 지나면 취소됩니다.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
2번의 경우 대체로 context패키지의 다음 함수를 이용하여 구현합니다:
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L713
// 입력으로 부모 context와 key, val을 받습니다.
// 자식 context에 key에 val을 저장합니다.
func WithValue(parent Context, key, val any) Context
한편, 위의 context.WithCancel, context.WithTimeout, context.WithValue함수는 parent라는 부모 context를 인자로 받습니다. 그렇다면 최상위 부모 context를 만드는 함수가 필요해보이는데, 이는 context 패키지의 다음 함수를 이용합니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L211
func Background() Context
다음 함수를 이용하면 아무 동작도 하지 않는 부모 context를 생성할 수 있습니다. 이번에는 예시를 살펴봅시다.
case1: 라이브러리 함수, 메서드 호출 제어
1번 상황의 예시로 grpc 클라이언트에서 요청을 보내는 상황에서 context가 활용됩니다. 만약 grpc를 api 패키지로 만든 경우, 다음과 같이 클라이언트를 사용할 수 있습니다.
// ...
func main() {
conn, err := grpc.NewClient("localhost:3000", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
// 에러 처리
}
c := api.NewDataClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
getRequest := &api.GetRequest{
Key: "key",
}
getResponse, err := c.Get(ctx, getRequest)
if err != nil {
// 에러 처리
}
// response에 대한 처리
// ...
}
또한 NATS 메세지 큐를 활용한 메세지 발행에서 다음처럼 context를 활용할 수 있습니다.
// ...
func main() {
// nats 연결
nc, err := nats.Connect("localhost:3000")
if err != nil {
// ... 에러 처리
}
// jetstream 생성
js, err := jetstream.New(nc)
if err != nil {
// ... 에러 처리
}
// 메세지 생성
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if _, err := js.Publish(ctx, "test_subject", []byte("test_payload")); err != nil {
// ... 에러 처리
}
}
case2: 정보 전파
한편, 2번 상황의 예시로 opentelemetry에서 자동 추적을 위해 사용하는 trace.Tracer를 이용하는 코드의 예시를 보겠습니다. tracer를 제대로 선언하는 코드가 포함되면 양이 너무 많아져 중간 과정을 생략했습니다(보통 오픈텔레메트리에서는 전역 tracer를 사용합니다. 아래 코드는 예시로만 참고하시면 됩니다).
package main
import (
"context"
"go.opentelemetry.io/otel/sdk/trace"
)
func main() {
// opentelemetry endpoint 연결
// ...
traceProvider := trace.NewTracerProvider()
// traceProvider 초기화
// ...
tracer := traceProvider.Tracer("some-service")
func(ctx context.Context) {
ctx, span := tracer.Start(ctx, "some function")
defer span.End()
// some function logic 진행
// ...
}(context.Background())
}
이때 추적을 수집하는 함수에서 인자로 context.Context를 받아 trace.Tracer의 Start메서드를 호출합니다. 해당 함수는 span이라는 함수 정보를 수집하는 인터페이스와 정보가 저장된 context를 만듭니다. 해당 메서드 내부의 동작은 다음과 같습니다.
// ...
func (tr *tracer) Start(ctx context.Context, name string, options ...trace.SpanStartOption) (context.Context, trace.Span) {
// ...
s := tr.newSpan(ctx, name, &config)
if rw, ok := s.(ReadWriteSpan); ok && s.IsRecording() {
// span에 대한 처리
// ...
}
// ...
return trace.ContextWithSpan(ctx, s), s
}
지금 중요한 건 opentelemetry 이해가 아니기 때문에 Start의 마지막 부분만 보면 ContextWithSpan이라는 함수를 호출하고 해당 함수는 다음과 같습니다.
// ...
// ContextWithSpan returns a copy of parent with span set as the current Span.
func ContextWithSpan(parent context.Context, span Span) context.Context {
return context.WithValue(parent, currentSpanKey, span)
}
context.WithValue라는 함수를 이용하여 현재 span의 key에 span을 등록하여 context를 사용할 수 있도록 합니다. 이렇게 수집된 span에 대한 data를 context를 통하여 전파하고 처리합니다.
context 인터페이스
context 인터페이스는 다음처럼 구성됩니다.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
그러나 지금까지의 사용 예시에서는 위의 4가지 메서드를 전혀 사용하지 않았습니다. 그저 WithCancel, WithTimeout, WithValue와 같은 함수로 context를 생성하고 필요한 경우 cancelFunc을 호출하여 취소를 주입하는 방식만 사용했습니다. 그렇다면 위의 4가지 메서드는 어디서 사용할까요? 이에 대한 예시로 case1의 nats 예시에서 js.Publish 함수 내부를 살펴보겠습니다. js.Publish 메서드를 호출하면
- js.Publish(https://github.com/nats-io/nats.go/blob/c7cf3452dd6359bdf40cbad0c39d900cbeba81e2/jetstream/publish.go#L148)
- js.PublishMsg(https://github.com/nats-io/nats.go/blob/c7cf3452dd6359bdf40cbad0c39d900cbeba81e2/jetstream/publish.go#L155)
- js.conn.RequestMsgWithContext(https://github.com/nats-io/nats.go/blob/c7cf3452dd6359bdf40cbad0c39d900cbeba81e2/context.go#L23)
- nc.requestWithContext(https://github.com/nats-io/nats.go/blob/c7cf3452dd6359bdf40cbad0c39d900cbeba81e2/context.go#L40)
의 순으로 함수 호출이 이루어지고 최종적으로 nc.requestWithContext가 호출됩니다. 이 함수는 다음과 같습니다.
func (nc *Conn) requestWithContext(ctx context.Context, subj string, hdr, data []byte) (*Msg, error) {
// ...
// context가 실행부터 에러가 있었다면 함수 실행 전 에러 처리
if ctx.Err() != nil {
return nil, ctx.Err()
}
var m *Msg
var err error
// If user wants the old style.
if nc.useOldRequestStyle() {
// ...
} else {
// 응답 채널 획득: mch는 메세지를 비동기로 리턴받는 채널
mch, token, err := nc.createNewRequestAndSend(subj, hdr, data)
if err != nil {
return nil, err
}
var ok bool
// 응답 채널과 context.Done 채널 중 select문을 활용하여 먼저 수신된 이벤트 우선 처리
select {
case m, ok = <-mch:
if !ok {
return nil, ErrConnectionClosed
}
case <-ctx.Done(): // 만약 context가 끝났다면 락을 획득하고 에러 처리
// ...
return nil, ctx.Err()
}
}
// Check for no responder status.
// ...
return m, err
}
위의 코드에서 context의 Err메서드와 Done메서드가 활용되었습니다. 이때 해당하는 메서드의 역할은 다음과 같습니다.
- context.Err(): 함수의 비동기 요청이 실행하기 전 context를 통해 외부에서 취소가 주입됐는지 우선 확인합니다. 만약 취소가 주입됐다면 요청을 전달하지 않고 미리 취소하고 리턴합니다.
- context.Done(): select문을 통해 외부에서 취소 주입을 비동기로 처리할 수 있도록 합니다. Done메서드는 채널을 리턴하고 해당 채널의 닫힘을 select문으로 검사하여 취소주입을 제어할 수 있습니다.
이처럼 context는 라이브러리 사용자가 직접 context의 메서드를 제어하는 것이 아닌 라이브러리 제공자 측에서 context의 메서드로 라이브러리 내의 흐름을 외부에서 제어할 수 있도록 합니다. case1과 같은 케이스에서 context를 이용할 때 크게 다음과 같이 구현합니다.
- 먼저 context.Done이나 context.Err로 context 종료를 판단하고,
- select문을 통해 context.Done의 채널을 이용한 취소 주입과 비동기 호출로 메세지 채널로부터 수신 중 우선인 이벤트를 처리합니다.
여기까지 context패키지와 context사용법을 알아봤습니다.
- context는 라이브러리 사용자 측의 라이브러리 함수를 원격 제어하거나 호출되는 모든 함수에 공통적으로 들어가는 값을 전달하는 데 사용합니다.
- 따라서 외부 통신 제어나 안전성 패턴 구현과 분산 추적에 사용됩니다.
- 만약 자신이 라이브러리 제공자라면 context패키지를 이용하여 사용자측에서 제어가능하도록 구현할 수 있습니다. 이때 context의 Err, Done과 같은 메서드가 사용됩니다.
context의 내부 구조
이번에는 context내부 구조에 대하여 살펴봅시다.
emptyCtx
먼저 기본이 되는 emptyCtx입니다. 빈 구조체에 메서드만 구현되어 있고 대부분 nil을 리턴합니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L177
type emptyCtx struct{}
func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
// nil 채널은 닫힘을 받지 않습니다.
// var a chan struct{} = nil
// select {
// case <-a:
// fmt.Println("1")
// default:
// fmt.Println("2") // 2만 출력됩니다.
// }
func (emptyCtx) Done() <-chan struct{} {
return nil
}
func (emptyCtx) Err() error {
return nil
}
func (emptyCtx) Value(key any) any {
return nil
}
기본 커넥스트를 그대로 감싼 컨텍스트로 backgroundCtx와 todoCtx가 있습니다. 각 컨텍스트는 context.Background(), context.TODO()로 생성할 수 있습니다. 이들은 대체로 루트 컨텍스트가 됩니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L195
type backgroundCtx struct{ emptyCtx }
type todoCtx struct{ emptyCtx }
func Background() Context {
return backgroundCtx{}
}
func TODO() Context {
return todoCtx{}
}
cancelCtx
cancelCtx는 cancel을 외부에서 호출할 수 있는 context를 의미합니다. WithCancel은 해당 context로 생성됩니다. 먼저 WithCancel 함수를 살펴보겠습니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L227
type CancelFunc func()
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L235
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L268
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}
WithCancel 함수를 살펴보면 withCancel이라는 내부 함수를 호출하고 해당 함수는 parent context 인터페이스로부터 *cancelCtx를 리턴합니다. cancelCtx는 WithCancel, WithTimeout, WithDeadline과 같은 함수에서 사용합니다. 이때 propagateCancel을 호출하여 parent context 취소 감지 이벤트를 등록합니다. *cancelCtx의 cancel과 propagateCancel을 살펴보기 전에 다른 메서드부터 먼저 확인해봅시다.
먼저 cancelCtx의 내부 필드입니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L421
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
}
먼저 Done 메서드입니다. Done 메서드가 호출되는 시점에 c.done 필드가 비어있을 경우 크기가 0인 채널을 생성 후 리턴합니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L438
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil { // 가져와서 nil이 아니면 그대로 리턴
return d.(chan struct{})
}
c.mu.Lock() // mutex잡고 channel load or store 진행
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
Err는 락을 획득하고 err를 리턴합니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L453
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
다음은 Value입니다. key가 cancelCtxKey 포인터로 들어오는 경우 자기 자신을 리턴합니다. context는 자식 context가 key에 대한 값을 먼저 조회한 뒤 있으면 리턴하고 없으면 부모의 Value()를 호출하도록 구현됩니다. 특이하게 context는 부모 자식관계를 가지고 있기 때문에 context는 어떤 종류의 context가 조상으로부터 구현됐는지 아래와 같은 방식으로 확인합니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L431
// Value를 호출할 때 key가 &cancelCtxKey이면 자기 자신을 리턴
// 이 부분 기억해야됨
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L759
// ctx의 타입을 확인하고 각 타입을 확인하는 key를 조회 후 리턴
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)
}
}
}
다음은 propagateCancel입니다. 부모 context가 취소됐을 때 자식에게 취소를 전파하는 함수입니다. 여기서는 부모 context가 *cancelCtx를 포함하는 경우를 살펴보겠습니다. 먼저 parent.Done()이 nil인 경우(emptyCtx 등)는 아무 동작 없이 리턴합니다. 부모의 done이 이미 닫혀 있으면 자식도 바로 종료합니다. parentCancelCtx로 부모가 cancelCtx를 소유한다면 p.children에 자신을 등록하여 부모 취소 시 자식에게도 취소가 전파될 수 있도록 합니다. 마지막으로 타입을 확인할 수 없는 경우 부모의 Done 채널이 닫히면 자식의 cancel을 호출합니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L462
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
c.Context = parent // parent context 가져옴
done := parent.Done()
if done == nil { // emptyCtx의 경우 아무것도 하지 않음
return // parent is never canceled
}
select {
case <-done:
// 부모 context가 이미 종료됐으면 자식도 종료
child.cancel(false, parent.Err(), Cause(parent))
return
default:
}
// 부모 컨텍스트가 *cancelCtx이면
if p, ok := parentCancelCtx(parent); ok {
// parent is a *cancelCtx, or derives from one.
p.mu.Lock()
if p.err != nil { // 부모에서 닫힘이 이미 발생했다면 바로 취소
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
}
// ...
// 만약 타입 추정이 불가한 경우 parent Done이 됐을 때 자식을 cancel
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
}
다음은 cancel입니다. cancel이 호출되면 Done 채널을 닫고 c.children에 등록된 하위 context를 모두 취소합니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L536
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
if cause == nil {
cause = err
}
c.mu.Lock() // 뮤텍스 획득 후
if c.err != nil { // 이미 이전에 c.err가 등록됐다면, 즉 cancel이 이전에 호출됐다면
c.mu.Unlock()
return // already canceled
}
// 아니면 새로 등록
c.err = err
c.cause = cause
d, _ := c.done.Load().(chan struct{}) // 채널 포인터 가져오기
if d == nil { // 없으면 닫힌 채널 넣고 아니면 있는 채널 닫기
c.done.Store(closedchan) // 이미 닫힌 채널을 생성해서 메모리 재사용
} else {
close(d)
}
for child := range c.children { // 모든 child context에 부모의 cancel 전파
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err, cause)
}
c.children = nil // child 배열 초기화
c.mu.Unlock()
// c.Context는 parent Context. parent에게 삭제 요청
if removeFromParent {
removeChild(c.Context, c)
}
}
마지막으로 parentCancelCtx 함수입니다. 이 함수는 Value로 cancelKey에 대한 값이 존재하는지 검사하여 부모 context가 cancelCtx를 구현하는지 검사합니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L372
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done() // 부모의 Done channel을 가져오고
if done == closedchan || done == nil { // 닫혔거나 nil(emptyCtx)이면 리턴
return nil, false
}
// 부모 Ctx의 Value를 호출하며 cancelCtxKey포인터 조회 후 형변환
// 만약 부모가 *&cancelCtx형이면 &cancelCtxKey에서 자신을 리턴함
// 따라서 부모 컨텍스트가 자기 자신을 *cancelCtx로 변환한다는 의미
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
// 부모 컨텍스트의 done 채널과 형변환된 결과의 p.done이 서로 다르면
// &cancelCtx의 구현이 아닌 걸로 판단
pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
return nil, false
}
return p, true
}
timerCtx
마지막은 timerCtx입니다. cancelCtx와 크게 다르지 않습니다. WithTimeout과 WithDeadline은 WithDeadlineCause를 호출합니다. WithDeadlineCause는 timer에 도달했을 때 AfterFunc을 이용하여 context의 취소 이벤트를 호출하는 방식으로 구현됩니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L689
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L611
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
return WithDeadlineCause(parent, d, nil)
}
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L618
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
// 부모의 데드라인이 더 빠른 경우, child는 WithCancel로 리턴
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
// 그럼 이 밑은 더 느린 경우 or 부모의 deadline이 없는 경우
c := &timerCtx{
deadline: d,
}
c.cancelCtx.propagateCancel(parent, c) // 부모 취소 이벤트 등록
dur := time.Until(d)
// 만약 바로 취소되는 이벤트면 종료 후 리턴
if dur <= 0 {
c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
c.mu.Lock()
defer c.mu.Unlock()
// timer등록 후 duration 이후 c.cancel을 호출
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, cause)
})
}
return c, func() { c.cancel(true, Canceled, nil) }
}
또한 cancel 함수에서 timer를 중지하는 부분이 추가됩니다.
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L648
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
// https://github.com/golang/go/blob/a10e42f219abb9c5b/src/context/context.go#L665
// cancelCtx와 크게 다르지 않음. timer 중지가 추가됨
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()
}
context 내부는 생각한 방식 그대로 동작하기 때문에 특별히 어렵게 느낄 부분은 없습니다. context 내부에서 인상 깊은 부분 이 정도 있는 것 같습니다.
- 부모 context를 자식의 필드에 등록하고 Value가 호출됐을 때 자식에서 부모 방향으로 재귀 호출을 통해 찾아가는 방식
- &cancelCtxKey를 이용한 타입 캐스팅을 회피하고 Value메서드를 활용하여 조상 중 cancelCtx가 구현되어 있는지 찾는 방식
- 이미 취소된 context인 경우 이미 닫힌 static 변수인 closedchan을 이용하는 방식
context는 내부보다 외부 구현이 좀 더 중요하다고 생각합니다. 특히 라이브러리를 제공하는 입장에서 context는 강한 위력을 발휘하는데 context를 이용하여 라이브러리의 함수 사이클을 외부에서 제어할 수 있다는 점입니다. 그러나 이 부분은 라이브러리 제공자가 모두 구현해야 합니다. 이를 구현하기 위해 다음 방법을 사용할 수 있습니다.
- 먼저 context.Done이나 context.Err로 context 종료를 판단하고,
- select문을 통해 context.Done의 채널을 이용한 취소 주입과 비동기 호출로 메세지 채널로부터 수신 중 우선인 이벤트를 처리합니다.
다음에는 이런 주제를 알아보겠습니다.
- context를 이용한 여러 안전성 패턴 구현
- sync.Cond 동작 원리(… 동작 원리 시리즈좀 빨리 끝내고 싶어요)
Leave a comment