Basic goroutine and channel

วันนี้มาเริ่มท่าง่ายๆกับ goroutine และ channel กันก่อน

Goroutine

เป็นวิธีการเรียก function ให้ทำงานโดยแยกออกจากลำดับการทำงานของ function ที่ไปเรียกใช้งาน ตัวอย่างเช่น เราอยากสร้างฟังก์ชันที่ให้นับทุกๆ 1 วินาที เราจะเขียนโค้ดได้ดังนี้

package main

import (
	"fmt"
	"time"
)

func SecondCounter() {
	for i := 1; i <= 20; i++ {
		time.Sleep(1 * time.Second)
		fmt.Println(i)
	}
}

func main() {
	SecondCounter()
}

ลองเล่นผ่าน Go Playground ได้ที่นี่ http://play.golang.org/p/jpM6QcJaKq

จะเห็นว่า เราต้องรอการนับแต่ละครั้งจบก่อน ถึงจะนับครั้งต่อไปได้ ทีนี้ถ้าเราต้องการมีตัวนับสองตัวล่ะ ให้ตัวนึงนับทุก 1 วินาที อีกตัวนับทุก 5 วินาที จะเห็นว่าเราไม่สามารถเรียก function สองตัวนี้พร้อมกันได้ วิธีการที่จะทำได้สำหรับ Golang คือเรียกฟังก์ชันให้ทำงานเป็น Goroutine เพื่อให้แยกทำงานอิสระจาก flow การทำงานหลัก

วิธีการเรียกฟังก์ชันให้ทำงานแบบ Goroutine คือใช้ keyword ว่า go ข้างหน้าการเรียกฟังก์ชัน เช่น

package main

import (
	"fmt"
	"time"
)

func SecondCounter() {
	for i := 1; i <= 20; i++ {
		time.Sleep(1 * time.Second)
		fmt.Println(i)
	}
}

func main() {
	go SecondCounter()
}

แต่ถ้ารันโค้ดนี้จะพบว่าไม่แสดงอะไรออกมาเลย เพราะอย่างที่บอกไปว่าเมื่อเรียกแบบ Goroutine มันจะแยกอิสระ ทำให้ ณ ตรงที่เรียกไม่จำเป็นต้องรอให้การทำงานเสร็จก่อน จะทำคำสั่งต่อไปเลย ซึ่งเมื่อไม่มี ก็จบฟังก์ชัน main ไปเลย

จะเห็นว่าเมื่อใช้ Goroutine แยกการทำงานแล้ว ก็ต้องมีของเพื่อช่วยให้ sync กันได้ว่า Goroutine ที่แยกไปทำงานเสร็จหรือยัง นั่นละคือที่มาของสิ่งต่อมาคือ channel

 

Channel

channel สำหรับ Go แล้วค่อนข้างตรงตามชื่อ คือ เป็นช่องทางการสื่อสารกันระหว่าง Goroutine นั่นเอง แทนที่เราจะใช้ตัวแปร global ธรรมดาที่ให้ goroutine ใช้ร่วมกันเพื่อ sync ข้อมูลกัน เราจะใช้ ข้อมูลแบบ channel ในการสื่อสารแทน เพื่อลดปัญหาที่มักเกิดกับการใช้ตัวแปรร่วมกันของการทำงานแบบ concurrency ที่เคยมีมาก่อนหน้านี้ การสร้างตัวแปร channel ทำได้โดยใช้ keyword chan ตามด้วย ประเภทข้อมูลที่จะใช้ส่งผ่าน channel เช่น

var ch chan int

ch เป็น channel ของ int

ส่วนการสร้าง channel จะใช้คำสั่ง make เช่น

ch := make(chan int)

ทีนี้จะโค้ดการนับเมื่อกี้ที่เป็น Goroutine เราจะลองใช้ channel ของ bool เพื่อให้ function main รอให้ function SecondCounter ทำงานเสร็จก่อนถึงค่อยจบการทำงานของ main ได้ดังนี้ (ลองรันบน Playgournd ที่ลิ้งนี้ http://play.golang.org/p/KwtMagnp5Q)

package main

import (
	"fmt"
	"time"
)

var done chan bool = make(chan bool)

func SecondCounter() {
	for i := 1; i <= 20; i++ {
		time.Sleep(1 * time.Second)
		fmt.Println(i)
	}
	done <- true
}

func main() {
	go SecondCounter()
	<-done
}

จากโค้ดนี้จะเห็นว่าเราใช้สัญลักษณ์ <- แทนการส่งข้อมูลเข้า และเอาข้อมูลออกจาก channel สังเกตุได้ง่ายๆจากทิศทางของตัวแปร channel ถ้าตัวแปร channel อยู่ด้านซ้าย ค่าที่ส่งอยู่ด้านขวาคือส่งข้อมูลเข้า ถ้าตัวแปร channel อยู่ด้านขวา คือรอเอาข้อมูลออกจาก channel ตัวอย่างของเราเมื่อ main เรียกฟังก์ชันให้ทำงานเป็น goroutine ไปแล้ว main เองจะสั่งให้รอเอาข้อมูลออกจาก chan ถ้าไม่มี main จะหยุดอยู่ตรงบรรทัดนั้นจนกว่าจะมีออกมา ทำให้โปรแกรมหลักเรารอจนกว่า SecondCounter ทำงานเสร็จเพราะ SecondCounter เมื่อทำงานเสร็จทำการส่งค่า true เข้าไปใน channel นั่นเอง

ทีนี้เราก็สามารถสร้างฟังก์ชันในการนับได้สองตัวพร้อมกันแล้ว ลองสร้างอีกตัวให้นับทุก 5 วินาที แล้วเรียกเป็น goroutine ทำงานไปพร้อมกับตัวที่นับ 1 วินาที ได้ดังนี้ (ลองรันบน Playgournd ที่ลิ้งนี้ http://play.golang.org/p/9yj4oiMVnv)

package main

import (
	"fmt"
	"time"
)

var oneDone chan bool = make(chan bool)
var fiveDone chan bool = make(chan bool)

func SecondCounter() {
	for i := 1; i <= 20; i++ {
		time.Sleep(1 * time.Second)
		fmt.Println("Second ", i)
	}
	oneDone <- true
}

func FiveSecondCounter() {
	for i := 1; i <= 4; i++ {
		time.Sleep(5 * time.Second)
		fmt.Println("FiveSecond ", i)
	}
	fiveDone <- true
}

func main() {
	go SecondCounter()
	go FiveSecondCounter()
	<-oneDone
	<-fiveDone
}