9.4.3 الگو Wait For Task

9.4.3 الگو Wait For Task

9.4.3.1 توضیحات #

الگوی Wait For Task یکی از ساده‌ترین و در عین حال پراستفاده‌ترین الگوهای همزمانی در Go است که برای منتظر ماندن تا اتمام یک فرآیند یا تسک معین کاربرد دارد. در این الگو معمولاً یک goroutine برای انجام کاری خاص راه‌اندازی می‌شود و پس از اتمام، از طریق یک channel به goroutine اصلی سیگنال پایان کار یا حتی داده‌ی تولیدشده را منتقل می‌کند. این روش به شما امکان می‌دهد همزمان چند کار مستقل را اجرا کنید و به صورت مجزا منتظر پایان هرکدام باشید، یا دقیقاً در لحظه‌ای مشخص بدانید یک تسک خاص تمام شده است و می‌توانید ادامه برنامه را اجرا کنید.

در رایج‌ترین شکل این الگو، یک کانال (معمولاً از نوع chan struct{} یا یک کانال بافر نشده) ایجاد می‌شود تا فقط نقش ارسال سیگنال (بدون دیتا) را بازی کند. برای مثال، یک goroutine عملیات طولانی یا I/O را انجام می‌دهد و پس از اتمام، با ارسال یک مقدار خالی (مثلاً done <- struct{}{}) به channel، پایان کار را اطلاع می‌دهد؛ main goroutine نیز با دریافت از channel (<-done) منتظر می‌ماند تا کار کامل شود. اگر علاوه بر سیگنال، نیاز به انتقال داده نیز باشد، می‌توان کانال را از نوع داده‌ی مورد انتظار ساخت تا خروجی همزمان با سیگنال ارسال شود.

کاربرد این الگو بسیار وسیع است؛ مثلاً در انجام یک کار زمان‌بر و اطلاع به UI یا سیستم دیگر، هماهنگی بین تسک‌های موازی، یا مدیریت صحیح پایان عملیات‌های async. همچنین اگر بخواهید چندین تسک موازی را اجرا کنید و منتظر اتمام همه آن‌ها بمانید، می‌توانید برای هر تسک یک channel جدا بسازید یا از sync.WaitGroup استفاده کنید (الگویی ترکیبی از Wait For Task و WaitGroup). این شیوه نه تنها باعث خوانایی و سادگی کنترل جریان برنامه می‌شود، بلکه از مشکلات رایج همزمانی (مانند race condition) نیز جلوگیری می‌کند و در عمل، ابزاری سریع و idiomatic برای سینک کردن تسک‌ها در Go به شمار می‌رود.

در نهایت، ترکیب این الگو با ساختارهای دیگر (مانند context یا select) امکان مدیریت پیشرفته‌تر، پیاده‌سازی تایم‌اوت، کنسل کردن عملیات و حتی مدیریت خطا را به سادگی فراهم می‌کند. این ویژگی‌ها سبب شده الگوی Wait For Task تقریباً در تمام پروژه‌های تولیدی Go، از پردازش ساده تا سیستم‌های توزیع‌شده، به شکل گسترده‌ای استفاده شود.

9.4.3.2 دیاگرام #

sequenceDiagram participant Main as Main Goroutine participant Task as Task Goroutine participant Chan as Done Channel Main->>Task: راه‌اندازی goroutine برای اجرای تسک Task->>Task: انجام عملیات (مثلاً: I/O یا پردازش) Task->>Chan: ارسال سیگنال پایان (done) Main->>Chan: منتظر دریافت سیگنال پایان Chan-->>Main: دریافت سیگنال و ادامه اجرای برنامه

9.4.3.3 نمونه کد #

 1package main
 2
 3import (
 4	"fmt"
 5	"time"
 6)
 7
 8// تعریف ساختار نتیجه کار
 9type TaskResult struct {
10	Data string
11	Err  error
12}
13
14func main() {
15	done := make(chan TaskResult)
16	go task(done)
17
18	result := <-done
19
20	if result.Err != nil {
21		fmt.Println("Task failed:", result.Err)
22		return
23	}
24	fmt.Println("Task complete!")
25	fmt.Println("Result:", result.Data)
26}
27
28func task(done chan<- TaskResult) {
29	fmt.Println("Task started...")
30	time.Sleep(2 * time.Second) // شبیه‌سازی کار زمان‌بر
31
32	// شبیه‌سازی موفقیت/خطا
33	if time.Now().Unix()%2 == 0 {
34		done <- TaskResult{
35			Data: "Some useful data",
36			Err:  nil,
37		}
38	} else {
39		done <- TaskResult{
40			Data: "",
41			Err:  fmt.Errorf("خطا در اجرای تسک"),
42		}
43	}
44}
1$ go run main.go
2Task started...
3Task complete!
4Result: Some useful data

در این مثال بهبود‌یافته، یک پیاده‌سازی حرفه‌ای و واقعی‌تر از الگوی Wait For Task در Go را مشاهده می‌کنید. هدف این است که هم‌زمانی، انتقال نتیجه یا خطا، و مدیریت کامل جریان کار به ساده‌ترین و امن‌ترین شکل انجام شود. ابتدا در تابع main یک کانال از نوع TaskResult ایجاد شده که این ساختار می‌تواند هم داده خروجی (در صورت نیاز) و هم خطا را در خود نگه دارد. سپس با اجرای goroutine تابع task، عملیات به صورت موازی شروع می‌شود و در این مدت، main منتظر می‌ماند تا نتیجه‌ای از کانال دریافت کند.

در تابع task ابتدا پیامی برای شروع کار چاپ می‌شود و سپس با دستور time.Sleep(2 * time.Second)، انجام یک کار زمان‌بر شبیه‌سازی می‌گردد. پس از آن، با یک شرط ساده، گاهی نتیجه موفقیت‌آمیز با داده خروجی و گاهی هم یک خطا به کانال ارسال می‌شود. این رویکرد نشان می‌دهد که چطور در سناریوهای واقعی، هم نتیجه و هم خطا را می‌توان به راحتی از طریق کانال به goroutine اصلی منتقل کرد تا کنترل کاملی روی مدیریت جریان و واکنش به خطاها داشت.

در بخش جمع‌آوری نتیجه، main با دریافت مقدار از کانال، ابتدا بررسی می‌کند که آیا خطایی رخ داده یا خیر؛ اگر خطا وجود داشته باشد، پیام مناسب چاپ شده و اجرای برنامه خاتمه می‌یابد. در غیر این صورت، پیام موفقیت و داده خروجی نمایش داده می‌شود. این ساختار باعث می‌شود کد همزمان کاملاً idiomatic، قابل گسترش و مناسب استفاده در پروژه‌های جدی باشد، چرا که به سادگی می‌توان مدیریت خطا، پردازش نتیجه و سینک شدن با کارهای async را با امنیت و شفافیت کامل انجام داد. این الگو پایه‌ای برای بسیاری از نیازهای تولیدی، مخصوصاً در هماهنگی و کنترل جریان بین goroutineها محسوب می‌شود.

9.4.3.4 کاربردها #

  • پردازش موازی حجم بالای داده‌ها: این الگو برای زمانی مناسب است که نیاز دارید حجم زیادی از داده‌ها را به بخش‌های کوچک‌تر تقسیم کرده و هر بخش را با یک goroutine مجزا به صورت موازی پردازش کنید. با استفاده از کانال و مکانیزم انتظار (wait)، می‌توانید تا اتمام کامل همه goroutineها منتظر بمانید و سپس مرحله بعدی برنامه را آغاز کنید. این روش، بهره‌وری پردازش را به شدت افزایش می‌دهد و زمان کل عملیات را کاهش می‌دهد.
  • برقراری چندین درخواست API به صورت همزمان: هنگام کار با سرویس‌های خارجی یا معماری‌های میکروسرویس، ممکن است نیاز باشد چندین تماس API را همزمان برقرار کنید و نتایج آن‌ها را جمع‌آوری نمایید. با الگوی Wait For Task می‌توانید هر درخواست را در یک goroutine ارسال کنید و سپس با جمع‌آوری سیگنال یا داده از کانال‌ها، فقط پس از دریافت همه پاسخ‌ها ادامه دهید؛ این کار latency سیستم را کاهش می‌دهد و تجربه کاربری بهتری فراهم می‌کند.
  • مدیریت ورودی کاربر به صورت غیرمسدودکننده: اگر بخواهید برنامه شما همچنان اجرا شود و در پس‌زمینه منتظر دریافت ورودی از کاربر باشید، می‌توانید یک goroutine مخصوص برای دریافت ورودی کاربر ایجاد کنید و پس از دریافت ورودی، سیگنالی از طریق کانال ارسال کنید. این الگو موجب می‌شود برنامه اصلی بلاک نشود و بتواند همزمان کارهای دیگری انجام دهد تا زمانی که ورودی کاربر فراهم شود.
  • منتظر آماده‌سازی منابع حیاتی: گاهی قبل از اجرای منطق اصلی برنامه نیاز است مطمئن شوید منابعی مثل اتصال پایگاه داده، باز شدن فایل یا ارتباط با سرویس خاصی برقرار شده است. با راه‌اندازی یک goroutine برای آماده‌سازی این منابع و ارسال سیگنال پس از آماده شدن، می‌توانید با اطمینان و همزمانی صحیح، اجرای برنامه را کنترل کنید و فقط در زمان آماده بودن منبع به مرحله بعد بروید.
  • مدیریت انجام کارهای پس‌زمینه: می‌توانید از این الگو برای اجرای یک عملیات در پس‌زمینه (مثلاً جمع‌آوری لاگ، بروزرسانی کش یا هر کار زمان‌بری که مستقیم به کاربر نمایش داده نمی‌شود) استفاده کنید و پیش از شروع مراحل بعدی یا خاموش شدن برنامه، مطمئن شوید این تسک به پایان رسیده است. این کار به شما کنترل کامل بر هماهنگی بخش‌های مختلف سیستم می‌دهد.