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. أولًا تستدعي للتحقق هل يمكن إلغاء السياق الأب، إذا لم يمكن تعود مباشرة

    go
    done := parent.Done()
    if done == nil {
    	return // parent is never canceled
    }
  2. ثم تتحقق هل السياق الأب قد أُلغي بالفعل، إذا كان كذلك تُلغي جميع السياقات الأبناء

    go
    select {
    case <-done:
    	// parent is already canceled
    	child.cancel(false, parent.Err(), Cause(parent))
    	return
    default:
    }
  3. تحاول تحويل السياق الأب لنوع cancelCtx، إذا نجحت تضيف السياق الحالي لـ 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، إذا نجحت تسجل طريقة إلغاء السياق الحالي في 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، عند استقبال إشارة تُلغي السياق الابن

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

ثم طريقة cancelCtx.cancel هي المسؤولة نهائيًا عن إلغاء السياق الابن

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

عمليتها كالتالي:

  1. التحقق هل السياق الحالي قد أُلغي

    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. عبور وإعلام السياقات الأبناء

    go
    for child := range c.children {
    	child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()
  4. أخيرًا حسب المعامل المُمرر تُقرر هل تحتاج للحذف من السياق الأب

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

الانتهاء

WithTimeout و WithDeadline كلاهما سياقان مع وقت انتهاء، كلاهما من نفس النوع، فقط اختلاف في دلالة الاستخدام، وكلاهما مبني على 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. فحص وقت الانتهاء، إذا كان وقت انتهاء الأب أسبق من الوقت الحالي، فالأب سيلغى قبل السياق الحالي، لذا تُنشئ سياقًا من نوع 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، ونشره للسياق الابن

    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 تضبط إلغاء السياق الحالي عند وقت الانتهاء

    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، ثم توقف السياق الابن أيضًا

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، وباقي تطبيقات السياق تدخل هذه الدالة للحصول على القيم، مثلًا

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 الحالي، تُعيد val المقابل للمفتاح.
  • *cancelCtx: إذا طابق key قيمة cancelCtxKey، تُعيد cancelCtx الحالي، أي تُعيد نفسها.
  • withoutCancelCtx: تعني Context بدون وظيفة إلغاء، إذا طابق key قيمة cancelCtxKey، تُعيد nil.
  • *timerCtx: إذا طابق key قيمة cancelCtxKey، تُعيد cancelCtx المرتبط به.
  • backgroundCtx و todoCtx: عادةً أنواع خاصة من Context لا تحمل أي قيم إضافية، عند مواجهة هذين النوعين تعيد nil مباشرة.
  • إذا كان نوعًا غير معروف، تستمر في استدعاء طريقة Value للبحث.

ملخص

جوهر هذه السياقات هو cancelCtx، من خلاله تُنشر إشارات الإلغاء، وباقي أنواع السياق وبعض أنواع السياق الخارجية كلها طبقة فوق طبقة، وعلى هذا الأساس تُنفذ وظائف متنوعة.

Golang تم تحريره بواسطة www.golangdev.cn