آموزش

تفاوت goroutine و Thread سیستم‌عامل در Go

یک goroutine خیلی سبک‌تر از یک thread سیستم‌عامل است — ولی سبک‌بودن به این معنی نیست که concurrency را رایگان می‌کند.

تحریریه‌ی دینا کد
نویسنده
۴ دقیقه مطالعه۰ بازدید
اشتراک گذاری۰

جواب کوتاه: goroutine یک ریسمان اجرا در سطح خودِ runtime گو است، نه سطح سیستم‌عامل — استک اولیه‌اش چیزی حدود ۲ کیلوبایت است و رشد می‌کند، در برابر یک تا چند مگابایت برای هر thread سیستم‌عامل. برای همین می‌شود صدها هزار goroutine باز کرد، ولی همان عدد thread، عملاً سیستم را از پا درمی‌آورد.

صدهزار goroutine، بدون تنظیم خاصی

govar wg sync.WaitGroup
const n = 100000
for i := 0; i < n; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
    }()
}
wg.Wait()

این برنامه بدون خطا و بدون تنظیم هیچ پارامتری تمام می‌شود. runtime گو خودش goroutineها را روی تعداد کمی OS thread (بر اساس GOMAXPROCS، معمولاً برابر تعداد هسته‌ها) زمان‌بندی می‌کند — این همان چیزی است که به آن M:N scheduling می‌گویند: میلیون‌ها goroutine (M) روی تعداد کمی thread واقعی (N).

سبک‌بودن، امنیت در برابر data race نمی‌دهد

gocounter := 0
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        counter++
    }()
}
wg.Wait()

با پرچم -race:

$ go run -race race.go
WARNING: DATA RACE
Read at 0x00c000114018 by goroutine 7:
  main.main.func1() race.go:12
Previous write at 0x00c000114018 by goroutine 5:
  main.main.func1() race.go:12

هزار goroutine همزمان روی یک متغیر counter++ می‌نویسند — که خودش سه عملیات جدا است (خواندن، جمع‌زدن، نوشتن) — و بدون هماهنگی، دو goroutine می‌توانند همزمان همان مقدار قدیمی را بخوانند و یکی از افزایش‌ها گم شود. go run -race دقیقاً همین تصادم را ردیابی و گزارش می‌کند.

راه درست: sync.Mutex یا کانال

govar mu sync.Mutex
counter := 0
go func() {
    defer wg.Done()
    mu.Lock()
    counter++
    mu.Unlock()
}()

یا با کانال، بدون قفل صریح: هر goroutine نتیجه‌اش را در یک کانال می‌فرستد و یک goroutine جمع‌کننده تک‌به‌تک می‌خواند. فلسفه‌ی گو همین است: «به‌جای اشتراک حافظه برای ارتباط، ارتباط بگیرید تا حافظه را به اشتراک بگذارید.»

چه‌وقت واقعاً به thread نیاز دارید

تقریباً هیچ‌وقت مستقیم — مگر کد C را با cgo صدا بزنید که خودش thread سیستم‌عامل می‌خواهد. برای همه‌ی همزمانی معمولی برنامه، goroutine همان چیزی است که باید استفاده کنید.

روی چند thread واقعی زمان‌بندی شدند؟

gofmt.Println("NumCPU:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))

روی ماشینی که این مثال‌ها رویش اجرا شد: NumCPU: 11، GOMAXPROCS: 11. یعنی همان صدهزار goroutineی مثال اول، روی فقط ۱۱ thread واقعی سیستم‌عامل زمان‌بندی شدند — نه صدهزار thread. GOMAXPROCS سقف تعداد threadهایی است که هم‌زمان کد گو را اجرا می‌کنند؛ پیش‌فرضش برابر تعداد هسته‌هاست و به‌ندرت لازم است دستی عوضش کنید.

اگر می‌خواهید Go را با پروژه‌ی واقعی و بک‌اند یاد بگیرید، دوره‌ی Go همین الگوهای همزمانی را قدم‌به‌قدم تمرین می‌دهد.

تحریریه‌ی دینا کد
نویسنده

نظرها

اولین نفری باشید که نظر می‌دهد
برای ثبت نظر وارد شوید

سؤالتان را بپرسید یا تجربه‌تان را بنویسید — نویسنده‌ی مطلب پاسخ می‌دهد.

مطالب مرتبط

از همین دسته
آموزش

چرا Rust به‌جای segfault، خطای کامپایل می‌دهد

در C یا ++C یک اشاره‌گر نامعتبر معمولاً ماه‌ها بعد، در پروداکشن، خودش را نشان می‌دهد. در Rust همان اشتباه، همان لحظه‌ی کامپایل متوقفت می‌کند.

تحریریه‌ی دینا کد۵ دقیقه