1 การแนะนำ Goroutines
1.1 แนวคิดพื้นฐานของการเกิดขึ้นทุกขณะและการขึ้นทุกขณะ
การเกิดขึ้นทุกขณะและการขึ้นทุกขณะเป็นแนวคิดสองอย่างที่พบมากในการเขียนโปรแกรมแบบ multi-threaded ซึ่งใช้ในการอธิบายเหตุการณ์หรือการทำงานของโปรแกรมที่อาจเกิดขึ้นพร้อมกัน
- การเกิดขึ้นทุกขณะ หมายถึงหลายงานที่ถูกประมวลผลในกรอบเวลาเดียวกัน แต่มีงานเพียงหนึ่งงานที่กำลังประมวลผลในขณะใด งานจะสลับโดยรวดเร็วระหว่างกัน ทำให้ผู้ใช้มีความเห็นภาพของการทำงานพร้อมกัน การเกิดขึ้นทุกขณะเหมาะสำหรับซีพียูเดียว
- การขึ้นทุกขณะ หมายถึงหลายงานที่ประมวลผลจริง ๆ พร้อมกันในขณะเดียวกัน ซึ่งต้องการการสนับสนุนจากซีพียูหลายคอร์
ภาษา Go ถูกออกแบบด้วยการเกิดขึ้นทุกขณะในความคิดค้นหาเป็นหนึ่งในวัตถุประสงค์หลักของตัวเอง มันทำให้มีรูปแบบการเขียนโปรแกรมแบบ concurrent ที่มีประสิทธิภาพผ่าน Goroutines และ Channels ได้อย่างมีประสิทธิภาพ Go's runtime จัดการกับ Goroutines และสามารถกำหนดตารางการทำงานของ Goroutines บนเธรดระบบต่าง ๆ เพื่อให้ได้การประมวลผลแบบขึ้นทุกขณะ
1.2 Goroutines ในภาษา Go
Goroutines เป็นแนวคิดหลักในการกระทำโปรแกรมแบบ concurrent ในภาษา Go ซึ่งเป็นเธรดที่บริการโดยมอดูล runtime ของ Go จากมุมมองของผู้ใช้ มันคล้ายกับเธรด แต่ใช้ทรัพยากรน้อยกว่า และเริ่มต้นได้เร็วขึ้น
ลักษณะพิเศษของ Goroutines รวมไปถึง
- เบา: Goroutines ใช้พื้นที่หน่วยความจำสแต็คน้อยกว่าเธรด传统 และขนาดของสแต็คสามารถขยายหรือย่อได้ตามต้องการ
- ซักอัตราต้นทุนต่ำ: ค่าใช้จ่ายสำหรับการสร้างและทำลาย Goroutines ต่ำมากเมื่อเทียบกับเธรด传统
- กลไกสื่อสารอย่างง่าย: Channels ให้กลไกการสื่อสารอย่างง่ายและมีประสิทธิภาพระหว่าง Goroutines
- ออกแบบ non-blocking: Goroutines ไม่บล็อกพอให้ Goroutines อื่น ๆ ทำงานต่อไปในระหว่างการบรรลุกตราการทำให้ ตัวอย่างเช่น ในขณะที่ Goroutines หนึ่งให้ร่อยการทำงานรอ I/O Goroutines อื่น ๆ จะดำเนินการทำงานต่ออย่างต่อเนื่อง
2 การสร้างและการจัดการกับ Goroutines
2.1 การสร้าง Goroutine
ในภาษา Go คุณสามารถสร้าง Goroutine ได้โดยใช้คำว่า go
โดยหากคุณเติมคำว่า go
ไปก่อนการเรียกใช้ฟังก์ชัน ฟังก์ชันจะถูกดำเนินการอย่างไม่ล่ะเมื่ออยู่ใน Goroutine ใหม
ขอแสดงตัวอย่างง่าย ๆ ดังนี้
package main
import (
"fmt"
"time"
)
// กำหนดฟังก์ชันเพื่อพิมพ์ Hello
func sayHello() {
fmt.Println("Hello")
}
func main() {
// เริ่มทำ Goroutine ใหมโดยใช้คำว่า go
go sayHello()
// Goroutine หลัก รอเพื่อรองรับการทำงานของ sayHello
time.Sleep(1 * time.Second)
fmt.Println("ฟังก์ชันหลัก")
}
ในโค้ดข้างต้น การสร้างหฟังก์ชัน sayHello()
จะถูกดำเนินการใน Goroutine ใหม นี่หมายความว่าฟังก์ชัน main()
จะไม่รอเพื่อให้ sayHello()
หมดงานก่อนทำงานต่อไป ดังนั้นเราใช้ time.Sleep
เพื่อหยุด Goroutine หลัก เพื่ออนุญาตคำสั่งพิมพ์ใน sayHello
ทำงาน นี่เป็นเพียงเพื่อการแสดงการทำงาน ในการพัฒนาจริงเราจะใช้ channels หรือวิธีการต่าง ๆ ในการประสานการทำงานของ Goroutines ที่แตกต่างกัน
หมายเหตุ: ในการประยุกต์ใช้จริงการพักโดยใช้
time.Sleep()
ไม่ควรถูกใช้เพื่อรอให้ Goroutine ทำงานเสร็จ เนื่องจากมันไม่ใช่กลไกการประสานการทำงานที่เชื่อถือได้ถาวร
2.2 กลไกการตารางการทำงานของ Goroutines
ใน Go การจัดตารางการทำงานของ Goroutines จะถูกโยนให้ตารางการทำงานของ Go runtime จัดการซึ่งเป็นตารางการทำงานที่มีหน้าที่จัดสรรเวลาการทำงานบนตัวประมวลผลตรรอกิน่ม ตัวตารางการทำงานของ Go ใช้เทคโนโลยีการจัดตาราง M:N
(หลาย Goroutines ถูกแมปไปยังหลายตัวเธรดของระบบปฏิบัติการ) เพื่อให้ได้ประสิทธิภาพที่ดีกว่าบนซีพียูหลายคอร์
GOMAXPROCS และตัวประมวลผลทางตรรอหลอก
GOMAXPROCS
คือตัวแปรสภาพแวดล้อมที่กำหนดห้างยข้ามายงะถียาทงียจะแย่งด้สลพูาจตัานาำการจัรตารางการทำงานของตรรอหลอก Go โดยมีค่าเริ่มต้นเท่ากับจำนวนคอร์ของซีพียูลข้างเซอชนี้ ตรีรอหลอก Go มีหนึ่งตัวโรบือเอียงาดกะ์เส จ่งกาดเอียี่ยึตรใยบหเั้ทหชไหการจืัยำที่ไหยสใยงี่ยเอปับิใดำยเทืองในรงจันสรงี่แผลมำารปิัำยทดหดศ็ดำห่ง้ตำนายี่ยียแนสท้างด้ตขำทาณงำหกนางำมดปิงสีาร์พำำดแงกำกน่าิทใยุ่งีมมงะกำำปง่าตห่ิาดกุำ่า ย pkg/runtime.
การกำหนดตัวแปร GOMAXPROCS ในโค้ดด้านบน จะกำหนดจำนวนสูงสุดของหน่วยประมวลผลที่สามารถจัดตารางการทำงานของ Goroutines ได้ให้เท่ากับ 2 เสมอ แม้ว่าโปรแกรมจะรันบนเครื่องที่มีหลายคอร์ของ CPU แต่ Go runtime จะจำนวนเยอะที่สุดที่ใช้แบบบคัลด์าฟืเป็งู ดื่ยนึเแย์รุการจจางทขนสาุ์สดหปอุงอีงต้อมำใ็ท ทเง เืี่ปหใบเาณี่่ย ยไื่ิ่ีาทำื่้ี่ย ป่ีีาทงำ ก่าป่ง้ิ่า ตใั่ิิีย ทแ้ง้าาเฆ่าใตำดหำ
การดำเนินการของตัวตารางเวลา
ตัวตารางเวลาทำงานโดยใช้สามองค์ประกอบสำคัญ: M (เครื่องจักร), P (โปรเซสเซอร์), และ G (โกรูตีน) โดยแต่ละตัวแทนสิ่งต่าง ๆ ดังนี้: M แทนเครื่องจักรหรือเธรด แสดงให้เห็นถึงการทำงานของเธรดในระดับของการทำงานของเม็ด OS kernel thread P แทนทรัพยากรที่จำเป็นสำหรับการดำเนินการโกรูตีน แต่ละ P มีคิวโกรูตีนท้องถิ่น G แทนโกรูตีนที่ประกอบด้วยสแต็กการทำงาน อินสตรัคชันเซ็ตและข้อมูลอื่น ๆ
หลักการทำงานของตัวตารางเวลาของ Go คือ
- M ต้องมี P เพื่อดำเนินการ G หากไม่มี P M จะถูกส่งกลับไปยังแคชเธรด
- เมื่อ G ไม่ถูกบล็อกโดย G อื่น (เช่นในการเรียกระบบ) มันจะเรียกใช้งานบน M เดียวกันให้มากที่เป็นไปได้ เพื่อช่วยให้ข้อมูลท้องถิ่นของ G อยู่ในสถานะ 'ร้อน' หรือใช้เพื่อประโยชน์การใช้งานแคช CPU ที่มีประสิทธิภาพมากขึ้น
- เมื่อ G ถูกบล็อก M และ P จะแยกกัน และ P จะค้นหา M ใหม่หรือปลุก M ใหม่เพื่อรับใช้ G อื่น ๆ
go func() {
fmt.Println("สวัสดีจาก Goroutine")
}()
โค้ดด้านบนแสดงถึงการเริ่ม Goroutine ใหม่ซึ่งจะทำให้ตัวตารางเวลาเพิ่ม G ใหม่ลงในคิวสำหรับการดำเนินการ
การตัดสินใจล่วงหน้าในการตั้งตารางเวลาของ Goroutines
ในช่วงต้น Go ใช้การตัดสินใจร่วมมือ ซึ่งหมายความว่า Goroutines สามารถหลงเหลว Goroutines อื่น ๆ หากการดำเนินการนานๆ โดยไม่ให้อำนวยการควบคุม ตอนนี้ตัวตารางเวลาของ Go นำการตัดสินใจล่วงหน้ามาใช้ ทำให้ Goroutines ที่ทำงานนานสามารถหยุดพักเพื่อให้โอกาสให้ Goroutines อื่น ๆ ทำงานต่อ
การบริหารจัดการรอบชีวิตของ Goroutine
เพื่อให้มั่นใจในประสิทธิภาพและการทำงานที่ยั่งยืนของแอปพลิเคชัน Go การเข้าใจและการบริหารจัดการรอบชีวิตของ Goroutines เป็นสิ่งสำคัญ การเริ่ม Goroutines ง่าย แต่หากไม่มีการบริหารจัดการอย่างเหมาะสม อาจสร้างปัญหาเช่นการรั่วหน่วยความจำและเงื่อนไขแข่งขัน
เริ่ม Goroutines อย่างปลอดภัย
ก่อนการเริ่ม Goroutines โปรดตรวจสอบภาระงานและคุณสมบัติของรันไทม์ให้เข้าใจ โปรดตรวจสอบว่า Goroutines ควรมีจุดเริ่มและจุดสิ้นสุดชัดเจนเพื่อหลีกเลี่ยงการสร้าง "กอรูตินอเพน" ที่ไม่มีเงื่อนไขการสิ้นสุด
func worker(done chan bool) {
fmt.Println("กำลังทำงาน...")
time.Sleep(time.Second) // จำลองงานที่มีค่าแพง
fmt.Println("ทำงานเสร็จสิ้น")
done <- true
}
func main() {
// ที่นี่ใช้กลไกช่องสำหรับการส่งข้อความใน Go คุณสามารถคิดเป็นช่องการส่งข้อความพื้นฐาน ๆ และใช้ตัวดำเนินการ "<-" เพื่ออ่านและเขียนข้อมูลคิว
done := make(chan bool, 1)
go worker(done)
// รอให้ Goroutine ทำงานเสร็จ
<-done
}
โค้ดด้านบนแสดงวิธีหนึ่งในการรอให้ Goroutine ทำงานเสร็จโดยใช้ช่องสำหรับการส่ง "done"
หมายเหตุ: ตัวอย่างนี้ใช้กลไกช่องสำหรับการส่งข้อความใน Go ซึ่งจะถูกอธิบายในบทต่อไป
หยุด Goroutines
โดยทั่วไป การสิ้นสุดของโปรแกรมทั้งหมดจะสิ้นสุด Goroutines ทั้งหมดโดยอัตโนมัติ อย่างไรก็ตาม ในการให้บริการที่ทำงานนาน เราอาจต้องหยุด Goroutines โดยการกระทำ
- ใช้ช่องสำหรับส่งสัญญาณหยุด: Goroutines สามารถตรวจสอบช่องเพื่อตรวจสอบสัญญาณหยุด
stop := make(chan struct{})
go func() {
for {
select {
case <-stop:
fmt.Println("ได้รับสัญญาณหยุด กำลังปิดการทำงาน...")
return
default:
// ดำเนินการตามปกติ
}
}
}()
// ส่งสัญญาณหยุด
stop <- struct{}{}
-
ใช้แพ็คเกจ
context
ในการบริหารจัดการชีวิต:
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("ได้รับสัญญาณหยุด กำลังปิดการทำงาน...")
return
default:
// ดำเนินการตามปกติ
}
}
}(ctx)
// เมื่อต้องการหยุด Goroutine
cancel()
การใช้แพ็คเกจ context
ช่วยให้ควบคุม Goroutines ได้ยืดหยุ่นมากขึ้น มีความสามารถในการตั้งเวลาถอยพลิกและยกเลิก ในแอปพลิเคชันขนาดใหญ่หรือการบริการไมโคร บริหารจัดการโดยใช้ context
คือวิธีที่แนะนำ