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.
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
// 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ưaDone(), trả về một channel, dùng để thông báo tin nhắn kết thúcErr(), trả về lý do đóng contextValue(), 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 contextWithCancel,WithCancelCause, context có thể hủyWithDeadline,WithDeadlineCause, context có thời hạn chótWithTimeout,WithTimeoutCause, context có thời gian timeoutWithValue, context có thể truyền giá trị
Cụ thể đến triển khai thì chủ yếu là
timerCtxcancelCtxemptyCtx
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
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.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}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ề
godone := parent.Done() if done == nil { return // parent is never canceled }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
goselect { case <-done: // parent is already canceled child.cancel(false, parent.Err(), Cause(parent)) return default: }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àochildrencủa parent context.goif 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 }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àoAfterFunccủa parent contextgoif 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 }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 contextgogo 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
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
...
}Quy trình của nó như sau
Kiểm tra xem context hiện tại đã bị hủy chưa
goif 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 }Đóng channel
done, gửi thông báo đónggoc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }Duyệt thông báo cho các child context
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()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
WithTimeout và WithDeadline đề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
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}WithDeadlineCause负责 tạo context có thời hạn chót
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}Quy trình của nó như sau
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
cancelCtxgoif 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) }Xây dựng
timeCtx, và lan truyền đến child contextgoc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)Tính toán thời gian cutoff hiện tại, nếu đã cutoff thì trực tiếp hủy
godur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, cause) // deadline has already passed return c, func() { c.cancel(false, Canceled, nil) } }Nếu chưa cutoff, thì thông qua
time.AfterFuncthiết lập hủy context hiện tại tại thời gian cutoffgoc.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
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
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ụ
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
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ếukeykhớp vớikeycủa Context hiện tại, thì trả vềvaltương ứng vớikey.*cancelCtx: NếukeykhớpcancelCtxKey, thì trả vềcancelCtxhiệ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ếukeykhớpcancelCtxKey, thì trả vềnil.*timerCtx: NếukeykhớpcancelCtxKey, trả vềcancelCtxliên kết với nó.backgroundCtxvàtodoCtx: 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.
