چه زمانی باید از کانال های بافر شده استفاده کرد؟

توسط mrbardia72
زمان خواندن ~1 دقیقه
چه زمانی باید از کانال های بافر شده  استفاده کرد؟

کانال ها همراه با goroutines هسته اصلی سازوکار همزمانی Go را تشکیل می دهند که مبتنی بر  مدل CSP است.

اگرچه این سازوکارها روشهای بسیار مناسبی برای کنترل همزمان را  ارائه می دهند ، اما همچنین می توانند به صورت کاملاً بهینه مورد استفاده قرار گیرند و به طور بالقوه مشکلی را برای توسعه دهندگان ایجاد می کند.

اگر کانال unbuffered باشد ، فرستنده زمانی مسدود می شود که گیرنده مقداری را دریافت کند.

اگر کانال buffer باشد ، فرستنده فقط تا زمانی که مقدار در بافر کپی نشود مسدود می شود.

اگر بافر پر باشد ، این بدان معنی است که منتظر بماند تا برخی از گیرنده ها مقداری را بازیابی کنند.

چه زمانی نباید از کانالهای بافر شده استفاده کرد ، حتی اگر واقعاً نیاز باشد.

ابتدا ، بیایید مثال  را در نظر بگیریم:

func main() {
	ch := make(chan struct{})
	// quick source. could be from some external request
	go externalMessages(ch)
	// slow receiver
	for {
		<-ch
		log.Println("received message")
		// slow operation
		time.Sleep(time.Second * 3)
	}
}

func externalMessages(ch chan<- struct{}) {
	for {
		go func() {
			log.Println("created goroutine")
			ch <- struct{}{}
			log.Println("send message")
		}()
		time.Sleep(time.Second)
	}
}

در این مثال ما وانمود می کنیم که پیام ها را از منبع خارجی دریافت می کنیم. برای هر پیام جدید یک فرمول جدید وجود دارد که سعی دارد پیام را به کانال ch ما هدایت کند. در حلقه اصلی ، پیام جدید را می خوانیم 

این روش چه مشکلی دارد؟

وقتی این کد رو  اینگونه می نویسید ، به راحتی می بینیم که تعداد goroutines های موجود در برنامه دائماً در حال افزایش است. بگذارید اینطور بگوییم که با چنین کدی سرانجام متوجه می شوید  که مشکلی در کاربرد آنها وجود دارد. این می تواند به طور مداوم در حال افزایش تعداد goroutines در نمودار اندازه گیری  شود ، می تواند به طور مداوم در حال افزایش cpu و حافظه باشد یا اینکه  هنگام راه اندازی مجدد برنامه پیام های زیادی را از دست می دهد.

بنابراین ما می آیم کد فوق رو به صورت زیر تغیی می دهیم

func main() {

	ch := make(chan struct{}, 100000) // notice the buffer

	// quick source. could be from some external requests
	go externalMessages(ch)

	// slow receiver
	for {
		<-ch
		log.Println("received message")
		// slow operation
		time.Sleep(time.Second * 3)
	}

}


به نظر می رسد فقط با یک خط مشکل برطرف شده است. این برنامه برای مدتی با خوشحالی کار می کند ، اما سرانجام ، وقتی برنامه به زمان مشخصی رسید ، دوباره مشکل بوجود می آید.

 چرا استفاده از کانال بافر شده در این حالت بهترین ایده نبود؟

تنها راه برای دانستن اینکه چه تعداد objects در buffer وجود دارد len (channel) است ، اما از آنجا که ما در یک محیط همزمان کار می کنیم این تعداد می تواند به سرعت تغییر کند. این بدان معناست که ما واقعاً نمی توانیم از وضعیت buffer آگاهی داشته باشیم و حتی اگر آن را نیز بدانیم ، بازهم نمی توانیم آن را تغییر دهیم زیرا ظرفیت bufferقابل افزایش نیست.

پس راه حل بهتر چه بود؟ گیرنده را مسدود نمی کنیم.

func main() {

	ch := make(chan struct{})

	// quick source. could be from some external request
	go externalMessagesNonBlocking(ch)

	// slow receiver
	for {
		select {
		case <-ch:
			log.Println("received message")
			// slow operation
			go func() {
				time.Sleep(time.Second * 3)
				log.Println("slow operation ended")
			}()
		}
	}
}

func externalMessagesNonBlocking(ch chan<- struct{}) {
	for {
		go func() {
			log.Println("created goroutine")
			select {
			case ch <- struct{}{}: // successfully send the message
				log.Println("send message")
			default: // or <-timer.C for timeout
				// receiver was blocked, do something
			}
		}()
		time.Sleep(time.Second)
	}
}

  • اکنون به جای انجام همه کارها در حلقه اصلی ، load را به چندین goroutine منتقل می کنیم. کانال هرگز مسدود نمی شود.
  • ما همچنین می توانیم  graceful shutdown  را به برنامه خود  اضافه کنیم ، بنابراین کار آغاز شده را از دست نخواهیم داد. همچنین با افزودن تایم اوت به کارهای باعث می شود تا برای همیشه منتظر آنها نباشیم.
  • اگر نگران افزایش تعداد زیادی goroutine هستید که باعث کند شدن عملکرد اجرای شود ، می توانید از worker pool استفاده کنید.

زمان استفاده از کانال بافر

قانون خوب در مورد کانال های بافر این است: "فقط اگر می دانید از چه اندازه بافر باید استفاده می کنید ، بافر را به کانال اضافه کنید". اگر مطمئن نیستید که بافر باید 100 یا 1000 باشد ، احتمالاً با کانال غیر بافر  بهتر هستد استفاده کنید و می توانید به سرعت مشکل را پیدا کرده و برطرف کنید.

اما گاهی اوقات دقیقاً می دانید که به چه اندازه بافر نیاز دارید. به عنوان مثال هنگامی که شما در حال آزمایش هستید و دقیقاً دارای تعداد X مورد آزمایش هستید

نکته جالب دیگر این است که وقتی فقط می خواهید X goroutines به طور همزمان اجرا شوند. به عنوان مثال شما 16 پردازنده اصلی دارید و وظیفه شما سنگین است بنابراین فقط منطقی است که 16 goroutine را به طور موازی اجرا کنید. در موارد مشابه می توانید از الگوی semaphore  استفاده کنید:

// allowing only 5 goroutine at a time
var limit = 5
var throttler = make(chan struct{}, limit)

func loop() {
	for i := 0; i < 100; i++ {
		work(i)
	}
}

func work(id int) {
	throttler <- struct{}{} // block while full
	go func() {
		defer func() {
			<-throttler // read to release a slot
		}()
		log.Println(id, "received")
		time.Sleep(time.Second)
	}()
}

جمله ی پایانی

اگرچه قرار دادن بافر در هر کانال بسیار وسوسه انگیز است ، اما احتمال زیادی وجود دارد که برنامه شما به آن نیازی نداشته باشد. هنگام کار با کانال ها می توانید قانون اساسی را به خاطر بسپارید. فقط اگر می دانید باید از چه اندازه بافر استفاده کنید ، بافر را به کانال اضافه کنید.