همزمانی تنها یک ویژگی در Go نیست؛ بلکه پایه فلسفه طراحی آن است. برخلاف زبانهایی که به رشتهها (threads) و قفلها تکیه دارند که اغلب منجر به شرایط مسابقه پیچیده و سربار میشود، Go با معرفی گوروتینها و کانالها، مدیریت وظایف همزمان را به شکلی شیک و ایمن ممکن میسازد. برای توسعهدهندگان سطح متوسط تا پیشرفته، تسلط بر این الگوها برای ساخت خدمات بکاند با عملکرد بالا و مقیاسپذیر ضروری است. این پست به بررسی مفاهیم اصلی، الگوهای عملی و دامهای رایج همزمانی در Go میپردازد.
درک گوروتینها
یک گوروتین، رشته اجرای سبکوزنی است که توسط زماناجرای (runtime) Go مدیریت میشود، نه توسط سیستمعامل. این تمایز بسیار مهم است. در حالی که رشتههای سیستمعامل حافظه قابل توجهی مصرف میکنند و نیاز به تعویض زمینه دارند، گوروتینها تنها با چند کیلوبایت فضای پشته شروع به کار میکنند که به صورت پویا رشد و کاهش مییابد. شما میتوانید هزاران و حتی میلیونها گوروتین را روی یک ماشین واحد ایجاد کنید بدون اینکه منابع سیستم را مستهلک کنید.
ایجاد یک گوروتین به سادگی پیشقرار کردن کلمه کلیدی go قبل از فراخوانی یک تابع است. با این حال، مهم است که به خاطر داشته باشید گوروتین اصلی (که main() را اجرا میکند) به طور پیشفرض منتظر تمام شدن سایر گوروتینها نمیماند. اگر تابع اصلی خارج شود، تمام گوروتینهای دیگر بلافاصله، صرفنظر از وضعیتشان، متوقف میشوند.
کانالها: ارتباط بدون حافظه مشترک
یکی از مشهورترین شعارهای Go این است: "به اشتراکگذاری حافظه برای ارتباط استفاده نکنید؛ در عوض، حافظه را با ارتباط به اشتراک بگذارید." این امر از طریق کانالها محقق میشود. کانالها مانند لولههایی عمل میکنند که گوروتینهای همزمان را به هم متصل میکنند و به آنها اجازه میدهند مقادیر با انواع مشخص را ارسال و دریافت کنند. آنها راهی ایمن برای همگامسازی اجرا و جلوگیری از رقابت دادهها فراهم میکنند.
کانالها میتوانند بافر شده یا بدون بافر باشند. یک کانال بدون بافر، فرستنده را تا زمانی که گیرنده آماده باشد مسدود میکند که همگامسازی دقیقی را تضمین میکند. یک کانال بافر شده به فرستنده اجازه میدهد تا زمانی که بافر فضای خالی دارد پیش برود که میتواند نرخ انتقال را بهبود بخشد، اما نیاز به مدیریت دقیق دارد تا از بنبست یا نشت منابع جلوگیری شود.
الگوی عملی: گروههای کارگر
یکی از رایجترین و مفیدترین الگوهای همزمانی در Go، گروه کارگر (Worker Pool) است. این الگو تعداد گوروتینهای همزمان را محدود میکند که هنگام کار با وظایف سنگین مانند کوئریهای پایگاه داده یا فراخوانیهای API خارجی حیاتی است. بدون محدود کردن همزمانی، ممکن است پایگاه داده خود را با تعداد زیادی اتصال بیشازحد بارگذاری کنید.
فرض کنید در سценاریویی هستید که نیاز به پردازش لیست بزرگی از وظایف دارید. میتوانید تعداد ثابتی از کارگرها ایجاد کنید که وظایف را از یک کانال مشترک دریافت میکنند.
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
fmt.Printf("Worker %d started job %d\n", id, j)
// شبیهسازی کار
results <- j * 2
}
}
func main() {
const numJobs = 5
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// شروع کارگرها
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// ارسال وظایف
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// بستن نتایج پس از اتمام کار تمام کارگرها
go func() {
wg.Wait()
close(results)
}()
// جمعآوری نتایج
for r := range results {
fmt.Println("Received result:", r)
}
}
در این مثال، ما از sync.WaitGroup برای انتظار تا اتمام کار تمام کارگرها قبل از بستن کانال نتایج استفاده میکنیم. این تضمین میکند که هیچ دادهای از دست نمیرود و برنامه به طور تمیز متوقف میشود.
جلوگیری از بنبست و نشت منابع
با وجود سادگی آن، همزمانی در Go میتواند پیچیده باشد. یک دام رایج، بنبست (Deadlock) است که زمانی رخ میدهد که یک گوروتین منتظر عملیاتی روی کانال است که هرگز تکمیل نمیشود، زیرا گوروتین دیگری در حال ارسال یا دریافت نیست. مشکل دیگر، نشت گوروتینها است، جایی که گوروتینها به صورت نامحدود به اجرا ادامه داده و حافظه مصرف میکنند.
برای کاهش این خطرات، همیشه در نظر بگیرید که از بسته context استفاده کنید. زمینهها (Contexts) به شما امکان میدهند سیگنالهای لغو و مهلتها را در سراسر گوروتینها منتشر کنید، که تضمین میکند وظایف طولانیمدت را میتوان به آرامی متوقف کرد اگر بیش از حد طول بکشند یا اگر درخواست والد لغو شود.
نتیجهگیری
رویکرد Go در همزمانی، جعبهابزاری قدرتمند برای ساخت نرمافزارهای کارآمد فراهم میکند. با بهرهگیری از گوروتینها برای همزمانی سبکوزن و کانالها برای ارتباط ایمن، توسعهدهندگان میتوانند کدی بنویسند که هم از نظر عملکردی و هم از نظر خوانایی عالی باشد. با این حال، موفقیت نیاز به انضباط دارد. همیشه منابع را به دقت مدیریت کنید، از اصول همگامسازی مانند sync.WaitGroup و sync.Mutex به درستی استفاده کنید و لغو زمینه را برای ساخت سیستمهای مقاوم بپذیرید. با تمرین، این الگوها به عادت دوم تبدیل میشوند و به شما امکان میدهند از قدرت کامل قابلیتهای همزمان Go بهرهمند شوید.