Go – Goroutine

Goroutine là một hàm hoặc phương thức có thể chạy đồng thời với các hàm hoặc phương thức khác. Go sử dụng Goroutine như một cách để xử lý các tác vụ thực hiện đồng thời. Goroutine có thể coi như một thread nhẹ, chi phí tạo một Goroutine là nhỏ so với một thread. Do đó các ứng dụng Go thường có hàng nghìn Goroutine chạy đồng thời.

Để tạo mới một goroutine sử dụng câu lệnh go.

Để chạy một hàm như một goroutine, gọi hàm đó với câu lệnh go nằm ở trước, như ví dụ sau đây:

sum()     // gọi hàm chạy theo tuần tự và chờ khi hàm trả về
go sum()  // một goroutine chạy hàm sum bất đồng bộ và không cần đợi đến khi hàm kết thúc

Từ khoá Go làm cho hàm được gọi trả về ngay lập tức, đồng thời hàm bắt đầu chạy ngầm như một goroutine và phần còn lại của chương trình vẫn tiếp tục chạy. Hàm main của tất cả của tất cả chương trình Go được khởi chạy như một goroutine, nên tất cả các chương trình của Go sẽ hoạt động với ít nhất một goroutine.

Ưu điểm của Goroutine so với thread

  • Goroutine là vô cùng rẻ khi so sánh với một thread. Chúng chỉ vài kb trong kích thước stack và stack có thể tăng hoặc co lại tuỳ theo yêu cầu của ứng dụng, trong khi đó với trường hợp của thread kích thước stack sẽ được chỉ định và cố định
  • Goroutine được ghép với số lượng thread OS ít hơn. Có thể chỉ một thread trong chương trình với hàng nghìn Goroutine. Nếu bất kỳ Goroutine trong các khối thread yêu cầu đợi người dùng nhập, thì thread OS khác được tạo và Goroutine còn lại được chuyển sang thread OS mới. Tất cả những cái này sẽ được xử lý trong lúc chạy và chúng ta là các lập trình viên được trừu tượng hoá từ những chi tiết phức tạp này và cung cấp các API để chạy đồng thời.
  • Các Goroutine giao tiếp với nhau sử dụng các channel. Các channel theo thiết kế ngăn các điều kiện Race khi sử dụng bộ nhớ chung bộ nhớ Goroutine. Channel có thể hình dung như là một đoạn ống mà các Goroutine giao tiếp với nhau.

Tạo Goroutine

Từ khoá go được thêm trước mỗi lần gọi hàm responseSize.  3 hàm responseSize được chạy đồng thời và hàm http.Getcũng được gọi đồng thời. Chương trình sẽ không đợi cho tới khi một hàm trả về mà sẽ gửi ngay lập tức 2 request còn lại. Kết quả 3 kích thước được trả về in ra sớm hơn với Goroutine.

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

func responseSize(url string) {
	fmt.Println("Step1: ", url)
	response, err := http.Get(url)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Step2: ", url)
	defer response.Body.Close()

	fmt.Println("Step3: ", url)
	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Step4: ", len(body))
}

func main() {
	go responseSize("https://vngeeks.com")
	go responseSize("https://coderwall.com")
	go responseSize("https://stackoverflow.com")
	time.Sleep(10 * time.Second)
}

Chúng ta gọi thêm hàm time.Sleep – đợi 10 giây, để tránh việc hàm main thoát trước khi các goroutine kết thúc.

Kết quả

Step1:  https://vngeeks.com
Step1:  https://stackoverflow.com
Step1:  https://coderwall.com
Step2:  https://stackoverflow.com
Step3:  https://stackoverflow.com
Step4:  116749
Step2:  https://vngeeks.com
Step3:  https://vngeeks.com
Step4:  79551
Step2:  https://coderwall.com
Step3:  https://coderwall.com
Step4:  203842

Chờ Goroutine chạy xong

Kiểu WaitGroup của package sync, được sử dụng để chờ cho đến khi chương trình kết thúc tất cả các Goroutine được khởi chạy từ hàm main.

Phương thức Add được sử dụng để thêm bộ đếm cho WaitGroup.

Phương thức Done của WaitGroup được đặt lịch sử dụng câu lệnh defer để giảm bộ đếm WaitGroup

Phương thức Wait của kiểu WaitGroup đợi cho đến khi chương trình kết thúc tất cả các Goroutine

Phương thức Wait được gọi bên trong hàm main, sẽ đợi cho tới khi bộ đếm WaitGroup giảm về 0 và đảm bảo tất cả goroutines được gọi.

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"sync"
)

// WaitGroup được sử dụng để chờ cho tới khi goroutines chạy xong
var wg sync.WaitGroup

func responseSize(url string) {
	// Gọi hàm Done của WaitGroup để thông báo rằng goroutine hoàn thành.
	defer wg.Done()

	fmt.Println("Step1: ", url)
	response, err := http.Get(url)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Step2: ", url)
	defer response.Body.Close()

	fmt.Println("Step3: ", url)
	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Step4: ", len(body))
}

func main() {
	// Add với giá trị là 3, 1 cho mỗi goroutine.
	wg.Add(3)
	fmt.Println("Start Goroutines")

	go responseSize("https://vngeeks.com/")
	go responseSize("https://stackoverflow.com")
	go responseSize("https://coderwall.com")

	// Đợi cho tới khi goroutines kết thúc.
	wg.Wait()
	fmt.Println("Terminating Program")
}

Kết quả

Start Goroutines
Step1:  https://coderwall.com
Step1:  https://vngeeks.com
Step1:  https://stackoverflow.com
Step2:  https://stackoverflow.com
Step3:  https://stackoverflow.com
Step4:  116749
Step2:  https://vngeeks.com
Step3:  https://vngeeks.com
Step4:  79801
Step2:  https://coderwall.com
Step3:  https://coderwall.com
Step4:  203842
Terminating Program

Lấy giá trị từ Goroutine

Cách phổ biến nhất để lấy giá trị từ Goroutine là các channel. Channel là các ống dẫn được kết nối với các Goroutine đang xử lý đồng thời. Bạn có thể gửi các giá trị vào các channel từ một Goroutine và nhận các giá trị từ các Goroutine khác hoặc trong một hàm chạy tuần tự.

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"sync"
)

// WaitGroup được sử dụng để chờ cho tới khi goroutines chạy xong
var wg sync.WaitGroup

func responseSize(url string, nums chan int) {
	// Gọi hàm Done của WaitGroup để thông báo rằng goroutine hoàn thành.
	defer wg.Done()

	response, err := http.Get(url)
	if err != nil {
		log.Fatal(err)
	}
	defer response.Body.Close()
	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		log.Fatal(err)
	}
	// gửi giá trị cho unbuffered channel
	nums <- len(body)
}

func main() {
	nums := make(chan int) // Khai báo một unbuffered channel
	wg.Add(1)
	go responseSize("https://vngeeks.com", nums)
	fmt.Println(<-nums) // Đọc giá trị từ unbuffered channel
	wg.Wait()
	close(nums) // Đóng channel
}

Kết quả

79655

Chạy và dừng thực thi Goroutine

Sử dụng các channel chúng ta có thể chạy hoặc dừng thực thi Goroutine. Một channel kiểm soát việc truyền thông tin này bằng các hoạt động như một ống dẫn giữa các Goroutine.

package main

import (
	"fmt"
	"sync"
	"time"
)

var i int

func work() {
	time.Sleep(250 * time.Millisecond)
	i++
	fmt.Println(i)
}

func routine(command <-chan string, wg *sync.WaitGroup) {
	defer wg.Done()
	var status = "Play"
	for {
		select {
		case cmd := <-command:
			fmt.Println(cmd)
			switch cmd {
			case "Stop":
				return
			case "Pause":
				status = "Pause"
			default:
				status = "Play"
			}
		default:
			if status == "Play" {
				work()
			}
		}
	}
}

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	command := make(chan string)
	go routine(command, &wg)

	time.Sleep(1 * time.Second)
	command <- "Pause"

	time.Sleep(1 * time.Second)
	command <- "Play"

	time.Sleep(1 * time.Second)
	command <- "Stop"

	wg.Wait()
}

Kết quả

1
2
3
4
Pause
Play
5
6
7
8
9
Stop

Khắc phục điều kiện Race sử dụng hàm Atomic

Điều kiện Race xảy ra do các bất đồng bộ truy cập đến tài nguyên chung và thử đọc và ghi các tài nguyên đó cùng một lúc.

Hàm Atomic cung cấp một cơ chế khoá mức thấp cho các đồng bộ hoá truy cập đến các con trỏ và interger. Hàm Atomic đại khái được sử dụng để khắc phục điều kiện Race

Các hàm trong atomic dưới package sync cung cấp hỗ trợ để đồng bộ hoá goroutine bằng cách khoá các truy cập đến tài nguyên chung.

package main

import (
	"fmt"
	"runtime"
	"sync"
	"sync/atomic"
)

var (
	counter int32          // counter là một biến được tăng bởi tất cả goroutine.
	wg      sync.WaitGroup // wg sử dụng để chờ cho chương trình kết thúc toàn bộ

func main() {
	wg.Add(3) // Add giá trị 3, 1 cho mỗi goroutine.

	go increment("Python")
	go increment("Java")
	go increment("Golang")

	wg.Wait() //  chờ các goroutine kết thúc.
	fmt.Println("Counter:", counter)

}

func increment(name string) {
	defer wg.Done() // Gọi hàm Done của WaitGroup để thông báo rằng goroutine hoàn thành.

	for range name {
		atomic.AddInt32(&counter, 1)
		runtime.Gosched() // tiến hành thread và được đặt lại trong hàng đợi.
	}
}

Hàm AddInt32 từ package atomic đồng bộ hoá việc thêm vào các giá trị integer bằng cách thực thi  mà chỉ có một goroutine có thể thực hiện hoàn thành toán tử add này trong cùng một thời điểm. Khi các goroutine thử gọi hàm atomic bất kỳ, chúng tự động đồng bộ hoá lại các biến được tham chiếu.

Kết quả

Counter: 15

Mutex

mutex được sử dụng để tạo một phần quan trọng xung quanh code để đảm bảo rằng chỉ một goroutine tại một thời điểm có thể chạy đoạn code đó.

package main

import (
	"fmt"
	"sync"
)

var (
	counter int32          // counter là một biến được tăng bởi tất cả goroutine.
	wg      sync.WaitGroup // wg sử dụng để chờ cho chương trình kết thúc toàn bộ
	mutex   sync.Mutex     // mutex được sử dụng để định nghĩa phần quan trọng của code.
)

func main() {
	wg.Add(3) // Add giá trị 3, 1 cho mỗi goroutine.

	go increment("Python")
	go increment("Go Programming Language")
	go increment("Java")

	wg.Wait() // chờ các goroutine kết thúc.
	fmt.Println("Counter:", counter)

}

func increment(lang string) {
	defer wg.Done() // Gọi hàm Done của WaitGroup để thông báo rằng goroutine hoàn thành.

	for i := 0; i < 3; i++ {
		mutex.Lock()
		{
			fmt.Println(lang)
			counter++
		}
		mutex.Unlock()
	}
}

Phần quan trọng được định nghĩa bằng cách gọi đến Lock() và Unlock() ngăn chặn các hành động ảnh hưởng đến biến counter và đọc text của tên biến.

Kết quả

PHP stands for Hypertext Preprocessor.
PHP stands for Hypertext Preprocessor.
The Go Programming Language, also commonly referred to as Golang
The Go Programming Language, also commonly referred to as Golang
Counter: 4

 

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

Your email address will not be published.