Giới thiệu
Concurrency là một khái niệm quan trọng trong lập trình, và Golang (Go) đã đặt ra một tiêu chuẩn mới về cách xử lý đồng thời. Bài viết này sẽ cùng bạn khám phá tại sao nó là một phần quan trọng của ngôn ngữ này và làm thế nào nó giúp xây dựng ứng dụng hiệu quả, mạnh mẽ.
Concurrency là gì
1. Lập trình đồng thời (Concurrency) là khả năng phân chia và điều phối nhiều tác vụ khác nhau trong cùng một khoảng thời gian, việc chuyển qua chuyển lại tác vụ rất nhanh nhưng tại một thời điểm chỉ có thể xử lý một tác vụ. Ở trong Go mỗi task vụ này là 1 goroutine.
Goroutines là gì?
Goroutines là các luồng thực thi nhẹ lightweight execution thread do khởi tạo tốn rất ít bộ nhớ stack. Nó được quản lý bởi hệ điều hành, cho phép chúng ta thực hiện các tác vụ đồng thời một cách hiệu quả. Hiểu đơn giản, Goroutines bản chất là các hàm (function) được thực thi một các độc lập và đồng thời với nhau.
- Một chương trình Go có thể chứa hàng ngàn Goroutines xử lý đồng thời
- Trong mỗi chương trình Golang, luôn có một Goroutine chính, gọi là main Goroutine. Nếu thằng chính này dừng lại, thì tất cả những thằng khác (Goroutines) trong chương trình cũng sẽ dừng lại, không được chạy.
Việc khai báo Goroutines trong Golang thì cực kỳ đơn giản, không cần phải import bất kì package nào cả. Bạn chỉ cần thêm cái từ khóa go
trước lời gọi hàm mà bạn muốn chúng chạy đồng thời thôi.
func quan(){
// code
} // goroutines
// only using go keyword
go quan()
Để hiểu rõ hơn về Goroutines hãy cùng làm một ví dụ nữa
package main
import ( "fmt"
) func showName(s string) { for i := 0; i < 3; i++ { fmt.Println(s) }
} func main() { // 2 goroutine go showName("Quân") go showName("Troy")
}
Ouput:
Như ở ví dụ này, chúng ta mong muốn "Quân" và "Troy" đều phải được in ra 3 lần, nhưng ouput lại không có gì cả. Tại sao lại như vậy? Bởi vì, do cả 2 hàm showName
có dùng từ khóa go
nên nó là goroutine và khi hàm main được chạy nó thấy goroutine sẽ đẩy vào Local Run Queue và Go Runtime sẽ tiếp tục chạy code bên dưới ở hàm main, nhưng không còn code nào để chạy cả nên hàm main
sẽ dừng lại. Như có viết ở trên khi màn main
dừng thì tất cả goroutine khác đều không được chạy.
Vậy làm thế nào để hàm main
đợi 2 goroutine chạy xong mới dừng lại?
Đơn giản nhất là chúng ta sẽ tạm dừng hàm main 1 khoảng thời gian để 2 goroutine kia được chạy
package main
import ( "fmt" "time"
) func showName(s string) { for i := 0; i < 3; i++ { fmt.Println(s) }
} func main() { go showName("Quân") go showName("Troy") // Pause the execution for 1 second time.Sleep(1 * time.Second)
}
Ouput:
Troy
Troy
Troy
Quân
Quân
Quân
Hàm time.Sleep(1 * time.Second)
sẽ tạm dừng hàm main trong 1s. Nên 2 goroutine đã có thời gian để chạy và in ra được kết quả như chúng ta mong muốn. Đương nhiên, trong thực tế chúng ra không để biết goroutine cần bao nhiêu thời gian để để chúng ta Sleep tương ứng, mà chúng ta sẽ cần tìm hiểu thêm về WaitGroup, Channel để hàm main
đợi goroutine thực sự chạy xong mới dừng lại.
WaitGroup
Vẫn dùng ví dụ trên nhưng chúng ta sẽ sửa một chút, không dùng Sleep nữa
package main
import ( "fmt" "sync"
) func sayName(s string, wg *sync.WaitGroup) { // Báo cho hàm main biết là đã chạy xong defer wg.Done() for i := 0; i < 3; i++ { fmt.Println(s) }
} func main() { // Khởi tạo một biến kiểu sync.WaitGroup var wg sync.WaitGroup // thông báo thêm 2 goroutine cần phải đợi wg.Add(2) go sayName("Quan", &wg) go sayName("Troy", &wg) // Đợi cho tất cả các goroutine chạy xong wg.Wait() fmt.Println("Application end")
}
Ouput:
Troy
Troy
Troy
Quan
Quan
Quan
Application end
Vẫn ra ouput mong muốn mà không cần dùng Sleep. Mình sẽ giải thích 1 chút về cách hoạt động của WaitGroup trong ví dụ ở trên. Đầu tiên, ta khởi tạo một biến thuộc kiểu sync.WaitGroup (sync là một package trong Go). Sau đó, ta thông báo cho WaitGroup biết có bao nhiêu Goroutine cần phải đợi bằng cách sử dụng phương thức Add(n)
với n là số lượng Goroutine. Trong trường hợp này, chúng ta có 2 Goroutine nên sử dụng wg.Add(2)
. Tiếp theo, khi một Goroutine hoàn thành nhiệm vụ của nó, ta thông báo cho hàm main biết bằng cách gọi phương thức wg.Done()
. Từ khóa defer
được sử dụng để đảm bảo rằng một hàm sẽ được gọi vào cuối cùng của hàm chứa nó, trong trường hợp này là wg.Done()
. Cuối cùng, wg.Wait()
sẽ lock hàm main lại đợi goroutine báo đã chạy xong mới unlock và thực hiện in ra dòng "Application end".
Có một lưu ý là phải truyền biến sync.WaitGroup vào goroutine theo dạng tham trị &wg
nếu chỉ truyền bình thường wg
thì trong goroutine sẽ copy lại biến wg
điều này dẫn đến biến wg
ở trong goroutine và hàm main đang không phải là một và logic code sẽ không chạy đúng nữa.
Channel
Trong các ví dụ trên, chúng ta đã thấy các Goroutines chạy độc lập với nhau. Tuy nhiên, liệu có cách nào để chúng giao tiếp với nhau hay không? Câu trả lời là có, và đó là thông qua Channel.
- Mặc định, Channel là một kênh giao tiếp 2 chiều, có nghĩa là nó có thể được sử dụng để gửi và nhận dữ liệu.
- Channel này giúp 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. Điều này đảm bảo rằng dữ liệu không bị mất hoặc xảy ra xung đột khi nhiều Goroutines cố gắng truy cập cùng một lúc.
Ví dụ đơn giản về Channel
package main
import "fmt" func sayName(s string, ch chan string) { result := "" for i := 0; i < 3; i++ { result += s + " is my name, " } // gửi giá trị vào channel ch <- result
} func main() { // tạo channel có thể chứa string ch := make(chan string) go sayName("Quan", ch) // lấy giá trị từ channel fmt.Println(<-ch) fmt.Println("Application end")
}
Ouput:
Quan is my name, Quan is my name, Quan is my name, Application end
Trong đoạn code trên chúng ta có thể thấy việc giao tiếp cơ bản giữa goroutine sayName()
và main goroutine. sayName()
đã gửi dữ liệu vào vào Channel ch <- result
và main goroutines đã có thể nhận và sử dụng nó. Toán tử <-
đóng vai trò nhưng hướng chỉ dữ liệu sẽ đi từ đâu đến đâu.
Kết
Trong bài viết, chúng ta đã tìm hiểu cách sử dụng Goroutines. Từ cách khai báo và quản lý Goroutines đến cách đợi chúng hoàn thành, mỗi ví dụ đã giúp chúng ta hiểu rõ hơn về tính năng quan trọng này trong Go. Nhờ vào Goroutines, chúng ta có thể xây dựng các ứng dụng mạnh mẽ và linh hoạt, đồng thời tận dụng tối đa tài nguyên máy tính. Điều này làm tăng hiệu suất và khả năng mở rộng của ứng dụng.
Bài viết là sự tổng hợp của mình trong quá trình học Go, nếu có sai xót hay thắc mắc gì mong các bạn comment bên dưới. Cảm ơn bạn đã đọc.
Tài liệu tham khảo
https://www.youtube.com/playlist?list=PLlahAO-uyDzIVzBvRKwKUDjj2Iaq-5W9l
https://200lab.io/blog/goroutines-la-gi/
https://go.dev/tour/concurrency/1