, June 27, 2022

0 kết quả được tìm thấy

Goroutines là gì? Lập trình concurrency chưa bao giờ dễ như Golang


  •   6 min reads
Goroutines là gì? Lập trình concurrency chưa bao giờ dễ như Golang

Goroutines là gì?

Goroutines là một trong những tính năng đặc biệt nhất của Golang để lập trình Concurrency cực kỳ đơn giản. Goroutines bản chất là các hàm (function) hay method được thực thi một các độc lậpđồng thời nhưng vẫn có thể kết nối với nhau. Một cách ngắn gọn, những thực thi đồng thời được gọi là Goroutines trong Go (Golang).

Bản chất của Goroutine

Trong bất kỳ một chương trình Golang đều tồn tại ít nhất một Goroutine, gọi là main Goroutine. Nếu main goroutines này kết thúc, toàn bộ các goroutines khác trong chương trình cũng đều bị dừng và kết thúc ngay.

Goroutine bản chất là một lightweight execution thread (luồng thực thi gọn nhẹ). Vì thế việc sử dụng các Goroutines trong Golang có chi phí cực kì thấp so với cách sử dụng các Thread truyền thống (OS Thread).

Goroutine so với Thread

  • Goroutines có kích thước nhỏ hơn rất đáng kể so với Thread. Goroutines sử dụng 2KB memory stack, trong khi đó các OS Thread lên đến 2MB.
  • Goroutines có thể linh động tăng giảm bộ nhớ sử dụng trong khi đó OS Thread là cố định.
  • Một chương trìng Golang có thể có hàng trăm nghìn Goroutine trong khi Thread chỉ được vài trăm đến hàng nghìn.
  • Goroutines có thời gian khởi động nhanh hơn Thread.
  • Goroutines có thể giao tiếp an toàn với nhau thông qua các kênh trong Golang (Channel). Các channel hỗ trợ mutex lock vì thế tránh được các lỗi liên quan tới cùng ghi và đọc lên vùng dữ liệu chia sẻ (data race).
  • Goroutines có thể được ánh xạ và hoạt động trên nhiều OS threads thay vì là ánh xạ 1:1.

Cách khai báo một Goroutine

Bất kì một hàm nào trong Golang cũng đều có thể chạy đồng thời hay Goroutines với việc thêm vào từ khoá go.

Từ khoá go cũng chính là tên của ngôn ngữ Go, là first-class keyword. Nghĩa là trong Golang bạn không cần phải import bất kì một package nào để sử dụng nó.

func name(){
// statements
}

// using go keyword as the 
// prefix of your function call
go name()
Goroutine Syntax
package main
  
import "fmt"
  
func sayHello(name string) {
    for i := 0; i <= 5; i++ {
        fmt.Printf("Hello %s\n", name)
    }
}
  
func main() {
    // Goroutine
    go sayHello("Viet")
  
    // normal function
    sayHello("Nam")
}
Ví dụ cách gọi hàm với Goroutine
Hello Nam
Hello Nam
Hello Nam
Hello Nam
Hello Nam
Output của chương trình trên

Trong ví dụ trên, hàm sayHello("Viet") có từ khoá go phía trước nên sẽ là một Goroutine. Hàm này sẽ trả về ngay lập tức chứ không thực thi ngay. Sau đó hàm sayHello("Nam") được thực thi bình thường bởi main goroutine. Kết quả chúng ta thấy được "Hello Nam" được in ra 5 lần trên màn hình.

Vậy tại sao không thấy "Hello Viet" nào cả?! Vì khi kết thúc hàm sayHello("Nam"), main goroutine đã kết thúc và chương trình exit trước khi các Goroutine bên dưới nó được thực thi.

Mình sẽ cho chương trình Sleep một giây trước khi kết thúc:

package main

import (
	"fmt"
	"time"
)

func sayHello(name string) {
	for i := 0; i <= 5; i++ {
		fmt.Printf("Hello %s\n", name)
	}
}

func main() {
	// Goroutine
	go sayHello("Viet")

	// normal function
	sayHello("Nam")

	time.Sleep(time.Second)
}
Thêm hàm Sleep 1s trước khi kết thúc chương trình
Hello Nam
Hello Nam
Hello Nam
Hello Nam
Hello Nam
Hello Viet
Hello Viet
Hello Viet
Hello Viet
Hello Viet
Kết quả đã đúng như mong đợi

Sử dụng Goroutine với anonymous function Golang

Chúng ta có thể khai báo goroutine cho một đoạn code như sau:

package main

import (
	"fmt"
	"time"
)

func main() {
	go func() {
		for i := 0; i <= 5; i++ {
			fmt.Println(i)
		}
	}()

	time.Sleep(time.Second)
}

Một hàm vô danh (anonymous function) được thực thi ngay có dạng func(){ ... }(). Khi thêm go vào trước nó sẽ được một anonymous Goroutine. Cực kỳ đơn giản.

Vấn đề capture variable trong Goroutine Golang

Đây là một vấn đề mà rất nhiều Gopher (Golang developer) gặp phải khi sử dụng Goroutines. Chúng ta thử xem xét đoạn code dưới đây nha:

package main

import (
	"fmt"
	"time"
)

func main() {
	for i := 1; i <= 100; i++ {
		go func() {
			fmt.Println(i) // biến i ở đây là một pointer
		}()
	}

	time.Sleep(time.Second)
}

Khi chạy đoạn code trên bạn sẽ phát hiện rất nhiều giá trị i bị trùng lặp. Đúng ra phải là các giá trị từ 1 đến 100 được in ra theo thứ tự ngẫu nhiên và không trùng.

Nếu một hàm sử dụng biến ở ngoài nó thì biến đó sẽ được capture, hay có thể hiểu chỉ là một tham chiếu đến biến ở ngoài mà thôi, không phải giá trị. Vì các Goroutines không được thực thi ngay, dẫn đến giá trị i ở ngoài thay đổi thì khi thực thi nó sẽ in ra i đã bị thay đổi.

Để tránh điều này, chúng ta sẽ copy giá trị i cho từng function Goroutine:

package main

import (
	"fmt"
	"time"
)

func main() {
	for i := 1; i <= 100; i++ {
		go func(value int) {
			fmt.Println(value) // value ở đây độc lập với i ở ngoài
		}(i) // value i được copy ở đây
	}

	time.Sleep(time.Second)
}

Kết quả sẽ là 1-100 in ra thứ tự ngẫu nhiên và không trùng nhau.

Sử dụng Gosched() để force schedule Goroutines

Việc thực thi và phân bổ tài nguyên giữa các Goroutines được runtime Golang quản lý. Trong đại đa số trường hợp chúng ta không cần phải quan tâm đến nó.

Dưới đây sẽ là một cách để tránh một Goroutine chiếm process khi làm việc với vòng lặp. Hãy cùng xem xét ví dụ sau đây:

package main

import (
	"fmt"
	"time"
)

func main() {
	go func() {
		for i := 1; i <= 50; i++ {
			fmt.Println("I am Goroutine 1")
		}
	}()

	go func() {
		for i := 1; i <= 50; i++ {
			fmt.Println("I am Goroutine 2")
		}
	}()

	time.Sleep(time.Second)
}

Khả năng rất cao các bạn sẽ thấy "I am Goroutine 1" hoặc "I am Goroutine 2" in ra liên tục mới đến cái còn lại.

Thực ra đây cũng là vấn đề mình thường thấy khi review code Golang. Điều này khiến ứng dụng chạy kém hiệu quả, chiếm dụng tài nguyên lớn và gây thắc cổ chai nghiêm trọng.

Để giải quyết vấn đề này, các bạn có thể dùng 2 cách:

  1. Sleep một chút ở mỗi vòng lặp, chấp nhận hy sinh performance do sleep thêm thời gian.
  2. Sử dụng hàm Gosched() của package runtime để schedule goroutine ngay lập tức sau mỗi vòng lặp.
package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	go func() {
		for i := 1; i <= 50; i++ {
			fmt.Println("I am Goroutine 1")
			runtime.Gosched()
		}
	}()

	go func() {
		for i := 1; i <= 50; i++ {
			fmt.Println("I am Goroutine 2")
			runtime.Gosched()
		}
	}()

	time.Sleep(time.Second)
}

Gosched() chỉ giúp chúng ta schedule goroutines. Việc chọn và thực thi Goroutine nào tiếp theo là do runtime quyết định. Cách này sẽ rât hiệu quả nếu bạn có rất nhiều Goroutines trong chương trình đang làm việc với vòng lặp.

Hy vọng qua bài viết này sẽ giúp các bạn hiểu rõ hơn về Goroutine, tự tin ứng dụng trong các service cũng như các buổi phỏng vấn tuyển dụng Golang Developer.

Bài viết liên quan

Buffered Channel là gì? Thường xuyên bị hỏi trong phỏng vấn Golang Dev

Buffered Channel là một channel trong Golang có khả năng lưu trữ được dữ liệu bên trong nó. Khả năng này được gọi là sức chứa (capacity)....

Buffered Channel là gì? Thường xuyên bị hỏi trong phỏng vấn Golang Dev
Golang Channel là gì? Các ứng dụng Channel trong Golang

Channel là kênh giao tiếp trung gian giữa các Goroutines có thể gởi và nhận dữ liệu cho nhau một cách an toàn thông qua cơ chế lock-free....

Golang Channel là gì? Các ứng dụng Channel trong Golang
Rust vs Go - Ngôn ngữ nào tốt nhất năm 2022

Rust và Go hiện là 2 ứng cử viên nặng ký và tin dùng cho các hệ thống lớn. Rust và Golang trông có vẻ tương tự nhau nhưng chúng cũng rất khác biệt...

Rust vs Go - Ngôn ngữ nào tốt nhất năm 2022
Ứng dụng Clean Architecture cho service Golang REST API

Trong bài viết này mình sẽ cung cấp một ví dụ cụ thể cho việc vận dụng nguyên tắc và tư duy Clean Architecture cho một service REST API Golang...

Ứng dụng Clean Architecture cho service Golang REST API
Golang Interface - Những lỗi sai thường gặp và giải pháp

Liệu bạn đã dùng Golang Interface đúng cách và hiệu quả? Bài viết này mình sẽ hướng dẫn chi tiết để các bạn dễ dàng sử dụng trong các service Golang nhé...

Golang Interface - Những lỗi sai thường gặp và giải pháp
You've successfully subscribed to 200Lab Blog
Great! Next, complete checkout for full access to 200Lab Blog
Xin chào mừng bạn đã quay trở lại
OK! Tài khoản của bạn đã kích hoạt thành công.
Success! Your billing info is updated.
Billing info update failed.
Your link has expired.