Skip to content

پیوست الف — ابزارها

هنگامی که در مسیر نوشتن کد concurrent قدم می‌گذارید، به ابزارهایی برای نوشتن برنامه و تحلیل آن از نظر correctness نیاز دارید و چند راهنمای مفید برای فهمیدن آنچه در برنامه‌هایتان رخ می‌دهد. خوشبختانه اکوسیستم Go مجموعهٔ غنی ابزارهایی هم از تیم Go و هم از community دارد!

این پیوست برخی از این ابزارها و چگونگی کمک آن‌ها به شما قبل، حین و بعد از توسعه را بحث می‌کند. چون این کتاب روی concurrency متمرکز است، گفت‌وگو را فقط به موضوعاتی محدود می‌کنم که در نوشتن یا تحلیل کد concurrent کمک می‌کنند. همچنین به‌طور مختصر نگاه می‌کنیم وقتی goroutineها panic می‌کنند چه می‌شود. زیاد اتفاق نمی‌افتد، اما خروجی بار اول که می‌بینید کمی گیج‌کننده است.

آناتومی خطای Goroutine

برای بهترین‌ها هم پیش می‌آید: دیر یا زود، برنامهٔ شما panic می‌کند. اگر خوش‌شانس باشید، هیچ انسان یا کامپیوتری آسیب نمی‌بیند و بدترین اتفاق این است که با انتهای بد یک stack trace روبه‌رو می‌شوید.

قبل از Go 1.6، وقتی goroutine panic می‌کرد، runtime stack trace همهٔ goroutineهای در حال اجرا را چاپ می‌کرد. گاهی تشخیص آنچه رخ داده دشوار (یا حداقل وقت‌گیر) بود. در زمان نگارش این کتاب، Go 1.6 و بالاتر با چاپ فقط stack trace goroutine panicکننده، کار را بسیار ساده‌تر می‌کند.

مثلاً وقتی این برنامهٔ ساده اجرا می‌شود:

go
package main

func main() {
    waitForever := make(chan interface{})
    go func() {
        panic("test panic")
    }()
    <-waitForever
}

stack trace زیر تولید می‌شود:

panic: test panic

goroutine 4 [running]:
main.main.func1()
    /tmp/babel-3271QbD/go-src-32713Rn.go:6 +0x65
created by main.main
    /tmp/babel-3271QbD/go-src-32713Rn.go:7 +0x4e
exit status 2

به محل وقوع panic اشاره می‌کند.

به محل start شدن goroutine اشاره می‌کند.

نام function در حال اجرا به‌عنوان goroutine را نشان می‌دهد. اگر function ناشناس باشد مثل این مثال، شناسهٔ خودکار و یکتا اختصاص می‌یابد.

اگر می‌خواهید stack trace همهٔ goroutineهایی که هنگام panic برنامه در حال اجرا بودند را ببینید، می‌توانید رفتار قدیمی را با تنظیم متغیر محیطی GOTRACEBACK روی all فعال کنید.

Race Detection

در Go 1.1، flag -race به اکثر دستورات go اضافه شد:

bash
$ go test -race mypkg    # test the package
$ go run -race mysrc.go # compile and run the program
$ go build -race mycmd   # build the command
$ go install -race mypkg # install the package

اگر developer هستید و فقط به روشی قابل‌اطمینان‌تر برای تشخیص race condition نیاز دارید، واقعاً همین کافی است. یک محدودیت استفاده از race detector این است که الگوریتم فقط raceهایی را پیدا می‌کند که در کدی که exercise شده وجود دارند. به همین دلیل، تیم Go توصیه می‌کند build برنامهٔ شما با flag race را تحت بار real-world اجرا کنید. این با افزایش احتمال exercise شدن کد بیشتر، احتمال یافتن race را بالا می‌برد.

گزینه‌هایی هم از طریق متغیرهای محیطی برای تنظیم رفتار race detector وجود دارد، اگرچه معمولاً مقادیر پیش‌فرض کافی‌اند:

LOG_PATH

این به race detector می‌گوید گزارش‌ها را در فایل LOG_PATH.pid بنویسد. می‌توانید مقادیر ویژه هم بدهید: stdout و stderr. مقدار پیش‌فرض stderr است.

STRIP_PATH_PREFIX

این به race detector می‌گوید ابتدای مسیر فایل‌ها در گزارش‌ها را حذف کند تا مختصرتر شوند.

HISTORY_SIZE

اندازهٔ history به ازای هر goroutine را تنظیم می‌کند که کنترل می‌کند چند دسترسی حافظهٔ قبلی به ازای هر goroutine به خاطر سپرده شود. بازهٔ معتبر مقادیر [0, 7] است. حافظهٔ تخصیص‌یافته برای history goroutine با HISTORY_SIZE برابر 0 از 32 KB شروع می‌شود و با هر مقدار بعدی دو برابر می‌شود تا حداکثر 4 MB با HISTORY_SIZE برابر 7. وقتی در گزارش‌ها «failed to restore the stack» می‌بینید، نشانه‌ای است که این مقدار را افزایش دهید؛ با این حال، می‌تواند مصرف حافظه را به‌طور قابل‌توجهی بالا ببرد.

با این برنامهٔ ساده که اول در فصل ۱ دیدیم:

go
var data int
go func() {
    data++
}()
if data == 0 {
    fmt.Printf("the value is %v.\n", data)
}

این خطا را دریافت می‌کنید:

==================
WARNING: DATA RACE
Write by goroutine 6:
  main.main.func1()
      /tmp/babel-10285ejY/go-src-10285GUP.go:6 +0x44

Previous read by main goroutine:
  main.main()
      /tmp/babel-10285ejY/go-src-10285GUP.go:7 +0x8e

Goroutine 6 (running) created at:
  main.main()
      /tmp/babel-10285ejY/go-src-10285GUP.go:6 +0x80
==================
Found 1 data race(s)
exit status 66

goroutineی را نشان می‌دهد که تلاش می‌کند دسترسی حافظهٔ unsynchronized بنویسد.

goroutineی را نشان می‌دهد (در این مورد main goroutine) که تلاش می‌کند همان حافظه را بخواند.

race detector ابزار بسیار مفیدی برای تشخیص خودکار race condition در کد شماست. شدیداً توصیه می‌کنم آن را بخشی از فرایند continuous integration خود کنید. باز هم، چون race detection فقط raceهایی را که رخ می‌دهند تشخیص می‌دهد و دیدیم race conditionها گاهی trigger کردنشان دشوار است، باید به‌طور مداوم سناریوهای real-world را اجرا کند تا سعی کند یکی را trigger کند.

pprof

در codebaseهای بزرگ، گاهی دشوار است بفهمید برنامهٔ شما در runtime چگونه perform می‌کند. چند goroutine در حال اجراست؟ آیا CPUهای شما کاملاً استفاده می‌شوند؟ وضعیت مصرف حافظه چطور است؟ profiling راه خوبی برای پاسخ به این سؤالات است و Go بسته‌ای در standard library برای پشتیبانی از profiler به‌نام «pprof» دارد.

pprof ابزاری است که در Google ساخته شد و می‌تواند دادهٔ profile را هم در حین اجرای برنامه و هم با مصرف آمار runtime ذخیره‌شده نمایش دهد. نحوهٔ استفاده از برنامه در help flag آن به‌خوبی توضیح داده شده، پس اینجا فقط بستهٔ runtime/pprof را بحث می‌کنیم — به‌ویژه در ارتباط با concurrency.

بستهٔ runtime/pprof بسیار ساده است و profileهای از پیش تعریف‌شده برای hook و نمایش دارد:

  • goroutine — stack trace همهٔ goroutineهای فعلی
  • heap — نمونه‌گیری از همهٔ heap allocationها
  • threadcreate — stack traceهایی که به ساخت OS thread جدید منجر شدند
  • block — stack traceهایی که به block شدن روی primitiveهای همگام‌سازی منجر شدند
  • mutex — stack traceهای holderهای mutexهای contended

از منظر concurrency، بیشتر این‌ها برای فهمیدن آنچه در برنامهٔ در حال اجرا رخ می‌دهد مفیدند. مثلاً این goroutine می‌تواند در تشخیص goroutine leak کمک کند:

go
log.SetFlags(log.Ltime | log.LUTC)
log.SetOutput(os.Stdout)

// Every second, log how many goroutines are currently running.
go func() {
    goroutines := pprof.Lookup("goroutine")
    for range time.Tick(1*time.Second) {
        log.Printf("goroutine count: %d\n", goroutines.Count())
    }
}()

// Create some goroutines which will never exit.
var blockForever chan struct{}
for i := 0; i < 10; i++ {
    go func() { <-blockForever }()
    time.Sleep(500*time.Millisecond)
}

این profileهای built-in واقعاً می‌توانند در profiling و تشخیص مشکلات برنامه کمک کنند، اما البته می‌توانید profileهای سفارشی بنویسید که برای monitor کردن برنامه‌هایتان طراحی شده‌اند:

go
func newProfIfNotDef(name string) *pprof.Profile {
    prof := pprof.Lookup(name)
    if prof == nil {
        prof = pprof.NewProfile(name)
    }
    return prof
}

prof := newProfIfNotDef("my_package_namespace")