Trang chủ Kiến Thức Công Nghệ Những sai lầm thường thấy khi bạn sử dụng goroutines
Công Nghệ

Những sai lầm thường thấy khi bạn sử dụng goroutines

Chia sẻ
Những sai lầm thường thấy khi bạn sử dụng goroutines
Chia sẻ

Trong bài viết này, mình sẽ đề cập đến một số trường hợp và sự cố phổ biến mà bạn có thể gặp phải khi sử dụng goroutines và cách để giải quyết chúng.

1. Giới thiệu

Đầu tiên, goroutine là gì? Bản chất Golang là concurrent. Để đạt được tính concurrent, Go sử dụng các goroutines – các hàm hoặc phương thức chạy đồng thời với các hàm hoặc phương thức khác. Ngay cả hàm main của Golang cũng là một goroutine.

Goroutines có thể được xem như các lightweight thread, nhưng không giống như các thread, chúng không được quản lý bởi hệ điều hành mà bởi runtime của Golang.

Việc một ứng dụng Go có hàng trăm, thậm chí hàng nghìn goroutines chạy đồng thời là điều rất bình thường.

(Đọc thêm về goroutines tại đây)

Hãy bắt đầu với một ví dụ nhanh và tạo một tệp hello.go:

Golang

package main

import (
 "fmt"
 "time"
)

func hello() {
    fmt.Println("Hello")
}

func main() {
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

và output sẽ là:

Bash

$ go run hello.go
Hello
main function

Tuyệt vời, goroutine của chúng ta đã thực thi thành công.

Tuy nhiên, khi bạn bắt đầu thêm nhiều chức năng hơn vào các goroutines, bạn cũng có thể gặp phải một trong những trường hợp phổ biến dưới đây.

2. Vấn đề Waiting…

Hãy bắt đầu với một cái đơn giản:

Như bạn có thể nhận thấy, việc sử dụng time.Sleep rất phổ biến khi thể hiện chức năng cơ bản của goroutines.

Vậy tại sao sleep lại cần thiết ở đây? Hãy kiểm tra nó ngay lập tức mà không cần hàm time.Sleep.

Golang

package main

import (
 "fmt"
 "time"
)

func hello() {
    fmt.Println("Hello")
}

func main() {
    go hello()
    // time.Sleep(1 * time.Second)     // now it's commented out
    fmt.Println("main function")
}

$ go run hello.go
main function

Rất tiếc, hiện tại output của goroutine bị thiếu. Tại sao lại như vậy?

Bởi vì quá trình thực thi của chương trình bắt đầu bằng cách khởi tạo main package và sau đó call hàm main. Khi lệnh gọi hàm đó trả về, chương trình sẽ thoát. Nó không đợi các goroutines khác (non-main) hoàn thành.

Có nghĩa là khi hàm main kết thúc, nó sẽ thực thi, nó sẽ không đợi các goroutines khác kết thúc.

Vì vậy, bây giờ chúng ta đã hiểu sự cần thiết của việc đợi các goroutines khác kết thúc, có cách nào hiệu quả và hiệu quả hơn để đợi goroutines kết thúc, thay vì đoán xem sẽ mất bao lâu để kết thúc goroutines?

Có chứ! nó được gọi là WaitGroups.

WaitGroups cho phép chúng ta chặn cho đến khi tất cả các goroutines trong WaitGroups đó hoàn tất quá trình thực thi của chúng.

Một ví dụ về triển khai WaitGroup:

Golang

package main

import (
    "fmt"
    "sync"
)

func hello(wgrp *sync.WaitGroup) {
    fmt.Println("Hello")
    wgrp.Done()           /////// notifies the waitgroup that it 			finished
}

func main() {
    var wg sync.WaitGroup
    wg.Add(1)         /////// adds an entry to the waitgroup counter
    go hello(&wg)
    wg.Wait()  ////// blocks execution until the goroutine finishes
    fmt.Println("main function")
}

Chạy code

Bash

$ time go run hello.go 
Hello
main function
real 0m0.230s
user 0m0.240s
sys 0m0.099s

Tốt hơn và nhanh hơn, vì chúng ta không phải đợi một khoảng thời gian cố định.

3. Deadlocks

Có thể bạn đã từng gặp lỗi đáng sợ này trước đây

Bash

fatal error: all goroutines are asleep - deadlock!

Deadlock xảy ra khi một nhóm goroutines đang đợi nhau và không ai trong số đó có thể tiến hành.

Hãy nhớ rằng, main package cũng là goroutine.

Golang

package main

import (
    "fmt"
    "sync"
)

func hello(wgrp *sync.WaitGroup) {
    fmt.Println("Hello")
    wgrp.Done()           /////// removing the wgrp.Done will cause a 		deadlock
}

func main() {
    var wg sync.WaitGroup
    wg.Add(2)             ///// 2 as the value will cause a deadlock
    go hello(&wg)
    wg.Wait()  ////// blocks execution until the goroutine finishes
    fmt.Println("main function")
}
  1. wgrp.Done() đánh dấu việc thực thi chương trình goroutine đã kết thúc. Nếu bỏ qua điều này cũng sẽ gây ra deadlock.
  2. wg.Add() nhận số lượng goroutines mà chúng ta nên chờ đợi.

Những giá trị khả thi bao gồm:

0 và goroutine sẽ không thực thi

1 sẽ hoạt động như mong đợi

2 trở lên sẽ dẫn đến deadlock

Trong cả hai trường hợp, chúng ta sẽ gặp phải deadlock vì hàm main đợi quy trình khác hoàn thành quá trình thực thi của nó:

Trường hợp 1: Goroutine sẽ không bao giờ đánh dấu việc thực thi nó trên WorkGroup là xong.

Trường hợp 2: wg.Add sẽ tiếp tục chờ đợi nhiều goroutines hơn dự kiến ​​để chạy.

Một trường hợp khác mà bạn sẽ gặp deadlock là khi không có goroutines nào khác để nhận những gì người gửi gửi, vì điều này không thể xảy ra trong cùng một goroutine:

Golang

package main

import "fmt"

func main() {
    c := make(chan string)
    c <- "hello"
    fmt.Println(<-c)
}

thay vào đó, hãy làm điều này:

Golang

package main

import (
    "fmt"
)

func main() {
    c := make(chan string)
    go func() {
        get := <-c                     
        fmt.Println("get value:", get)
    }()
    fmt.Println("push to channel c")
    c <- "hello" // send the value and wait until it's received.
}

4. Kết quả bất ngờ

Thêm một vòng lặp for vào hỗn hợp:

Golang

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    players := []string{"James Harden", "Kyrie Irving", "Kevin Durant"}
    wg.Add(len(players))
	for _, player := range players {
        go func() {
            fmt.Printf("printing player %sn", player)
            wg.Done()
        }()
    }
    wg.Wait()
}

$ go run hello.go
printing player Kevin Durant
printing player Kevin Durant
printing player Kevin Durant

Huh? Có phải nó sẽ in các tên khác nhau mỗi lần lặp lại không?

Uhm.. đúng là như vậy, nhưng các goroutines được tạo bên trong vòng lặp for sẽ không nhất thiết phải thực thi tuần tự.

Mỗi quy trình bắt đầu ngẫu nhiên.

Cách giải quyết khá đơn giản, chúng ta sẽ chỉ chuyển mục hiện tại của lần lặp:

Golang

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    players := []string{"James Harden", "Kyrie Irving", "Kevin Durant"}
    wg.Add(len(players))
for _, player := range players {
        go func(baller string) { // add the current iterated player
            fmt.Printf("printing player %sn", baller)
            wg.Done()
        }(player) // add the current iterated player
    }
    wg.Wait()
}

$ go run hello.go
printing player James Harden
printing player Kevin Durant
printing player Kyrie Irving

5. Race conditions và chia sẻ data giữa các goroutine

Giờ đây, nó trở nên phức tạp và thú vị hơn một chút:

Hãy tưởng tượng rằng bạn có một ứng dụng ngân hàng, nơi khách hàng có thể gửi và rút tiền.

Miễn là ứng dụng là single thread và đồng bộ, sẽ không có bất kỳ vấn đề gì, nhưng điều gì sẽ xảy ra nếu ứng dụng của bạn tạo ra hàng trăm hoặc hàng nghìn goroutines?

Hãy xem xét tình huống này:

Một khách hàng có số dư là 100 đô la và gửi 50 đô la vào tài khoản của mình.

Một goroutines xem các giao dịch, đọc số dư hiện tại là 100 đô la và thêm 50 đô la vào số dư.

Nhưng khoan đã, đồng thời cũng có một khoản phí 80 đô la được áp dụng cho tài khoản của khách hàng để thanh toán hóa đơn của anh ta tại quán bar địa phương.

Goroutine thứ hai sẽ đọc số dư hiện tại khi đó là 100 đô la, trừ đi 80 đô la trong tài khoản và cập nhật số dư tài khoản.

Sau đó, khách hàng sẽ kiểm tra số dư tài khoản của mình và thấy rằng đó chỉ là 20 đô la thay vì 70 đô la, vì goroutine thứ hai đã ghi đè giá trị số dư khi xử lý giao dịch đó.

Để giải quyết vấn đề này, chúng ta có thể sử dụng Mutex.

Mutex? Mutex (loại trừ lẫn nhau) là một phương pháp được sử dụng như một locking mechanism để đảm bảo rằng chỉ có một Goroutine đang truy cập vào phần code quan trọng tại bất kỳ thời điểm nào.

Thêm Mutexes tại đây. Nó sẽ trông như thế này:

Golang

package main

import (
    "fmt"
    "sync"
)

var (
    mutex   sync.Mutex
    balance int
)

func init() {
    balance = 100
}

func deposit(val int, wg *sync.WaitGroup) {
    mutex.Lock()             // lock
    balance += val
    mutex.Unlock()           // unlock
    wg.Done()
}

func withdraw(val int, wg *sync.WaitGroup) {
    mutex.Lock()             // lock
    balance -= val
    mutex.Unlock()           // unlock
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(3)
    go deposit(20, &wg)
    go withdraw(80, &wg)
    go deposit(40, &wg)
    wg.Wait()
    fmt.Printf("Balance is: %dn", balance)
}

$ go run mutex.go
Balance is: 80

Hãy chú ý đến các lệnh mutex.Lock() command và mutex.Unlock() command để làm cho nó xảy ra.

Chúng ta vẫn sử dụng workgroup theo cách tương tự như đã giải thích trước đó.

Có một cách khác để giải quyết nó, lần này là sử dụng các channel.

Channel là các đường ống kết nối các tuyến concurrent goroutines. 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ị đó vào một goroutine khác.

Hãy nhớ rằng, hàm main cũng là một goroutine.

(Xem thêm các channel tại đây)

Trong ví dụ này, chúng ta sử dụng buffered channel.

Buffered channel này được sử dụng để đảm bảo rằng chỉ một goroutine có thể truy cập vào phần code quan trọng, là phần điều chỉnh số dư.

Golang

package main

import (
    "fmt"
    "sync"
)

var (
    balance int
)

func init() {
    balance = 100
}

func deposit(val int, wg *sync.WaitGroup, ch chan bool) {
    ch <- true
    balance += val
    <-ch
    wg.Done()
}

func withdraw(val int, wg *sync.WaitGroup, ch chan bool) {
    ch <- true
    balance -= val
    <-ch
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    ch := make(chan bool, 1)      // create the channel
    wg.Add(3)
    go deposit(20, &wg, ch)
    go withdraw(80, &wg, ch)
    go deposit(40, &wg, ch)
    wg.Wait()
    fmt.Printf("Balance is: %dn", balance)
}

$ go run buf.go 
Balance is: 80

Chúng ta đã tạo một buffered channel với dung lượng là1, vì chúng ta muốn sửa đổi số dư chỉ một lần cho mỗi operation và nó được chuyển cho các goroutines gửi/rút tiền.

Vậy chúng ta nên chọn cái nào?

Nói chung, sử dụng các channel khi các Goroutines cần giao tiếp với nhau và tắt tiếng khi chỉ một Goroutine truy cập vào phần code quan trọng.
Trong trường hợp này, cách tốt nhất là sử dụng Mutex.

Mình hy vọng bạn thấy bài viết này hữu ích.

Bài viết được lược dịch từ Reshef Sharvit.

Bài viết cùng chuyên mục
Tối ưu ứng dụng với cấu trúc dữ liệu cơ bản và bitwise
Công Nghệ

Tối ưu ứng dụng với cấu trúc dữ liệu cơ bản và bitwise

Trong bài viết này, 200Lab sẽ chia sẻ những trường hợp dễ...

Công Nghệ

So sánh Flutter vs React Native: Framework nào đáng học năm 2021

Điểm chung của Flutter, React Native đều là Cross-platform Mobile, build native...

HTTP/2 là gì? So sánh HTTP/2 và HTTP/1
Công Nghệ

HTTP/2 là gì? So sánh HTTP/2 và HTTP/1

Từ khi Internet ra đời, sự phát triển về các giao thức...

Upload File từ Frontend đến Backend mà rất nhiều bạn vẫn đang làm sai!!
Công Nghệ

Upload File từ Frontend đến Backend mà rất nhiều bạn vẫn đang làm sai!!

1. Client encode file (base64) rồi gởi về backend 200Lab đã từng...

Công Nghệ

React Native – Hướng dẫn làm việc với Polyline và Animated-Polyline trên Map

Vẽ đường đi trên bản đồ là một nghiệp vụ vô cùng...

Công Nghệ

Hybrid App và Native App: Những khác biệt to lớn

Bất cứ khi nào một công ty quyết định làm ứng dụng...

Web/System Architecture 101 – Kiến trúc web/hệ thống cơ bản cho người mới
Công Nghệ

Web/System Architecture 101 – Kiến trúc web/hệ thống cơ bản cho người mới

Đây là một kiến trúc cơ bản mà bất kì một người...

Công Nghệ

Tư duy kiến trúc thông qua các trò chơi mà rất nhiều bạn không biết

Tư duy kiến trúc là gì? Tư duy kiến trúc có thể...