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 โดยการกระทำ

  1. ใช้ช่องสำหรับส่งสัญญาณหยุด: Goroutines สามารถตรวจสอบช่องเพื่อตรวจสอบสัญญาณหยุด
stop := make(chan struct{})

go func() {
    for {
        select {
        case <-stop:
            fmt.Println("ได้รับสัญญาณหยุด กำลังปิดการทำงาน...")
            return
        default:
            // ดำเนินการตามปกติ
        }
    }
}()

// ส่งสัญญาณหยุด
stop <- struct{}{}
  1. ใช้แพ็คเกจ 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 คือวิธีที่แนะนำ