Facebook Pixel

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

13 Jun, 2023

Goroutines bản chất là các hàm được thực thi một các độc lập và đồng thời trong Golang cực kỳ đơn giản và gọn nhẹ so với các Thread truyền thống.

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

Mục Lục

Trong bài viết dưới đây, chúng ta sẽ cùng tìm hiểu về bản chất của Goroutines cũng như cách khai báo và chi tiết cách sử dụng Goroutines. Đầu tiên chúng ta cùng tìm hiểu qua về định nghĩa Goroutines là gì trước nhé!

Goroutines là gì?

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

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

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ình 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ó.

Go
func name(){
// statements
}

// using go keyword as the 
// prefix of your function call
go name()
Goroutine Syntax
Go
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
Bash
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.

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

Go
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
Bash
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

Note: Trong ví dụ trên sẽ xuất hiện trường hợp các không thấy bất kỳ dòng "Hello Viet" nào hoặc không đủ 5 lần. Nguyên nhân là trong đoạn code trên không có gì đảm bảo rằng hàm sayHello("Viet") chắc chắn sẽ được call sau khi sleep 1s. Chỗ này còn tuỳ vào runtime.

Để giải quyết triệt để vấn đề trên, chúng ta buộc phải dùng các phương thức đồng bộ như Mutext Lock hoặc cơ chế Channel block của Golang. Tuy nhiên vì để giữ tính đơn giản cho bài này, 200Lab sẽ sử dụng đoạn code trên.

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

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:

Go
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

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:

Go
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:

Go
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:

Go
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 đề 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.
Go
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.

Lời kết

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.

Còn nếu bạn thực sự cảm thấy khó khăn trong việc tự học. Thậm chí đã làm được những service cơ bản nhưng vẫn chưa tự tin cho những phần nâng cao thì có thể tham khảo khoá học Golang for Scalable Backend tại 200Lab nhé!

Tham khảo thêm:

Bài viết liên quan

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab