Skip to content

context

context dịch là ngữ cảnh, mục đích thiết kế ban đầu là dùng để truyền tín hiệu và một số dữ liệu đơn giản giữa nhiều coroutine, đặc biệt là giữa các coroutine cha con. Nó thường được sử dụng trong các kịch bản xử lý yêu cầu HTTP, lập lịch tác vụ, truy vấn cơ sở dữ liệu, v.v., đặc biệt là trong kiến trúc microservices, gRPC sử dụng context để truyền metadata, điều khiển liên kết qua tiến trình và mạng.

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)
    }
}

Trong trường hợp trên, thông qua context để truyền tín hiệu tiến trình, khi nhận được tín hiệu, chương trình sẽ tự thoát, đây cũng là một kịch bản ứng dụng của context.

Cấu trúc

context.Context không phải là một triển khai cụ thể, mà là một interface định nghĩa một nhóm phương thức

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(), trả về một thời hạn chót, và giá trị boolean biểu thị đã kết thúc chưa
  • Done(), trả về một channel, dùng để thông báo tin nhắn kết thúc
  • Err(), trả về lý do đóng context
  • Value(), lấy giá trị được chỉ định theo key

Trong thư viện chuẩn tổng cộng có mấy context khả dụng sau

  • Background, context trống, thường dùng cho node gốc của context
  • WithCancel, WithCancelCause, context có thể hủy
  • WithDeadline, WithDeadlineCause, context có thời hạn chót
  • WithTimeout, WithTimeoutCause, context có thời gian timeout
  • WithValue, context có thể truyền giá trị

Cụ thể đến triển khai thì chủ yếu là

  • timerCtx
  • cancelCtx
  • emptyCtx

Nên chức năng cốt lõi của nó có bốn điểm

  • Hủy
  • Thời hạn
  • Truyền giá trị
  • Lan truyền

Hiểu được mấy cái này, về cơ bản là đã明白 cách hoạt động của context.

Hủy

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
}

Cốt lõi của cancelCtx nằm ở phương thức propagateCancel, nó负责 lan truyền hành vi hủy đến các context cha con.

go
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	...
}
  1. Trước tiên nó sẽ gọi kiểm tra xem parent context có thể bị hủy không, không thì trực tiếp trả về

    go
    done := parent.Done()
    if done == nil {
    	return // parent is never canceled
    }
  2. Trả về kiểm tra xem parent context đã bị hủy chưa, nếu có thì hủy tất cả các child context

    go
    select {
    case <-done:
    	// parent is already canceled
    	child.cancel(false, parent.Err(), Cause(parent))
    	return
    default:
    }
  3. Thử chuyển đổi parent context thành loại cancelCtx, nếu thành công thì sẽ thêm context hiện tại vào children của parent context.

    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. Thử chuyển đổi nó thành loại afterFuncer, nếu thành công, thì sẽ đăng ký phương thức hủy context hiện tại vào AfterFunc của parent context

    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. Nếu vẫn không được, thì mở một coroutine riêng để lắng nghe channel Done, khi nhận được tín hiệu, sẽ hủy child context

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

Sau đó do phương thức cancelCtx.cancel cuối cùng负责 hủy child context

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

Quy trình của nó như sau

  1. Kiểm tra xem context hiện tại đã bị hủy chưa

    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. Đóng channel done, gửi thông báo đóng

    go
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	c.done.Store(closedchan)
    } else {
    	close(d)
    }
  3. Duyệt thông báo cho các child context

    go
    for child := range c.children {
    	child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()
  4. Cuối cùng dựa vào tham số truyền vào để判断 xem có cần xóa khỏi parent context không

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

Thời hạn

WithTimeoutWithDeadline đều là context có thời hạn chót, cả hai đều là một loại, chỉ là ngữ nghĩa sử dụng không giống nhau, và đều dựa trên cancelCtx

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

	deadline time.Time
}

WithDeadlineCause负责 tạo context có thời hạn chót

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

Quy trình của nó như sau

  1. Kiểm tra thời hạn chót, nếu parent deadline sớm hơn current deadline, thì parent context chắc chắn sẽ bị hủy trước current context, nên trực tiếp tạo context loại 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. Xây dựng timeCtx, và lan truyền đến child context

    go
    c := &timerCtx{
    	deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
  3. Tính toán thời gian cutoff hiện tại, nếu đã cutoff thì trực tiếp hủy

    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. Nếu chưa cutoff, thì thông qua time.AfterFunc thiết lập hủy context hiện tại tại thời gian cutoff

    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) }

Đối với timerCtx mà nói, phương thức hủy context của nó chỉ là dừng timer, và顺便 dừng child 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()
}

Truyền giá trị

valueCtx có thể truyền giá trị, lấy giá trị trong contexts

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}
}

Cốt lõi của nó nằm ở hàm value, các triển khai context cụ thể khác cũng sẽ đi vào hàm này để lấy giá trị, ví dụ

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)
}

Hàm value là một vòng lặp for lớn, thông qua việc liên tục đệ quy lên trên để tìm giá trị key được chỉ định

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)
		}
	}
}

Xử lý các loại Context khác nhau:

  • *valueCtx: Nếu key khớp với key của Context hiện tại, thì trả về val tương ứng với key.
  • *cancelCtx: Nếu key khớp cancelCtxKey, thì trả về cancelCtx hiện tại, biểu thị trả về chính nó.
  • withoutCancelCtx: Biểu thị Context không có chức năng hủy, nếu key khớp cancelCtxKey, thì trả về nil.
  • *timerCtx: Nếu key khớp cancelCtxKey, trả về cancelCtx liên kết với nó.
  • backgroundCtxtodoCtx: Thường là các loại Context đặc biệt không mang bất kỳ giá trị bổ sung nào, khi gặp hai loại này trực tiếp trả về nil.
  • Nếu là loại không xác định, thì tiếp tục gọi phương thức Value để tìm kiếm.

Tóm tắt

Mấy context này cốt lõi là cancelCtx, thông qua nó để lan truyền tín hiệu hủy, các loại context khác cũng như các loại context của bên thứ ba cũng đều là một lớp bọc một lớp, trên cơ sở này thực hiện vô số chức năng khác nhau.

Golang by www.golangdev.cn edit