Go:21---goroutine併發執行體

江南、董少發表於2020-11-16

一、Go併發方式

  • Go有兩種併發程式設計的風格,分別是:goroutine和通道(channel)
  • 它們支援通訊順序程式(CSP),CSP是一個併發的模式,在不同的執行體(goroutine)之間傳遞值,但是變數本身侷限於單一的執行體

二、goroutine介紹

  • 在Go裡,每一個併發執行的活動稱為goroutine
  • 考慮一個程式,它有兩個函式,一個做一些計算工作,另一個將結果輸出,假設它們不互相呼叫。順序程式可能呼叫一個函式,然後呼叫另一個,但是在有兩個或多個goroutine的併發程式中,兩個函式可以同時執行

goroutine與執行緒

  • 如果你使用過作業系統或者其他語言中的執行緒,可以假設goroutine類似於執行緒,Go在幕後使用執行緒來管理併發
  • goroutine和執行緒之間在數量上有非常大的差別,建立一個goroutine只需佔用幾KB的記憶體,因此即便建立數千個goroutine也不會耗盡記憶體。另外,建立和銷燬goroutine的效率也非常高

主goroutine

  • 當一個程式啟動時,只有一個goroutine來呼叫main函式,稱它為主goroutine

go語句

  • 一個go語句是在普通的函式或者方法呼叫前加上go關鍵字字首
  • go語句使函式在一個新建立的goroutine中呼叫,go語句本身的執行立即完成,不需要阻塞等待
  • 例如:
f()     // 呼叫f(), 需要阻塞等待返回

go f()  // 新建一個呼叫f()的goroutine, 程式不需要阻塞等待

三、goroutine的執行週期

  •  goroutine會一直執行,直到本身函式結束,或者主程式結束

演示案例

  • 下面是一個例子,在goroutine所執行的函式還沒有執行完的時候,main就執行結束了,因此goroutine也跟隨著結束
package main

import (
    "fmt"
    "time"
)

func slowFunc() {
    // 延時2秒
    time.Sleep(time.Second * 2)
    fmt.Println("sleeper() finished")
}

func main() {
    go slowFunc()

    fmt.Println("I am now shown straightaway!")
}
  • 上面程式的執行效果如下,可以看到slowFunc()還未執行完,但是由於main()函式執行結束了,因此goroutine也跟隨著結束

  • 我們可以修改main()函式來阻塞等待slowFunc()執行完,這種方法比較簡單
package main

import (
    "fmt"
    "time"
)

func slowFunc() {
    // 延時2秒
    time.Sleep(time.Second * 2)
    fmt.Println("sleeper() finished")
}

func main() {
    go slowFunc()

    fmt.Println("I am now shown straightaway!")
    time.Sleep(time.Second * 3) // 阻塞等待goroutine任務完成
}

四、演示案例(斐波那契)

package main

import (
    "fmt"
    "time"
)

func main() {
    // 不用阻塞等待, 開啟一個新的goroutine執行該函式
    go spinner(100 * time.Millisecond)

    // 計算斐波那契, 一段時間後返回
    const n = 45
    fibN := fib(n)
    fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}

func spinner(delay time.Duration) {
    for {
        for _, r := range `-\|/`{
            fmt.Printf("\r%c", r)
            time.Sleep(delay)
        }
    }
}

func fib(x int) int {
    if x < 2 {
        return x
    }
    return fib(x - 1) + fib(x - 2)
}
  • 當fib()函式沒有執行完的時候,一直執行spinner()函式列印內容

  • 當fib()函式執行完之後,main()函式結束,程式終止,列印內容,如下所示

五、演示案例(併發時鐘伺服器)

  • 現在我們有這樣一個服務端程式:等待客戶端的連線,當客戶端連線之後會每秒鐘向客戶端傳送一次當前時間
  • 下面我們有2種設計服務端:
    • 不使用goroutine::此時服務端只能接受一個客戶端的連線,只有噹噹前的客戶端斷開連線之後才可以處理下一個客戶端
    • 使用goroutine:服務端可以同時接收多個客戶端的連線,並併發的給所有客戶端傳送資料
  • 下面是一個客戶端程式netcat.go,可以連線到指定的服務端,從服務端接收到資料然後列印到標準輸出上
package main 

import (
    "io"
    "log"
    "net"
    "os"
)

func main() {
    // 連線服務端
    conn, err := net.Dial("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    mustCopy(os.Stdout, conn)
}

func mustCopy(dst io.Writer, src io.Reader) {
    // 把接收到的資料列印到標準輸出上
    if _, err := io.Copy(dst, src); err != nil {
        log.Fatal(err)
    }
}

不使用goroutine

  • 服務端的程式如下,命名為clock1.go
package main

import (
    "io"
    "log"
    "net"
    "time"
)

func main() {
    // 建立tcp服務端
    listener, err := net.Listen("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }

    for {
        // 阻塞接收客戶端的連線
        conn, err := listener.Accept()
        if err != nil {
            log.Print(err)
            continue
        }
        // 接收到一個客戶端之後, 呼叫該函式處理該客戶端
        handleConn(conn)
    }
}

func handleConn(c net.Conn) {
    defer c.Close()
    for {
        // 向客戶端傳送當前內容, 內容為當前時間 
        _, err := io.WriteString(c, time.Now().Format("15:04:05\n"))
        if err != nil {
            return
        }
        // 休眠1秒
        time.Sleep(1 * time.Second)
    }
}
  • 執行上面的服務端程式,程式會阻塞等待客戶端的連線:

  • 使用上面的客戶端程式netcat去連線服務端,下面開啟了2個客戶端,但是由於服務端只能處理一個客戶端的請求,因此下面左側現開啟的客戶端正在接收資料,右側的客戶端只能阻塞等待

  • 當我們結束左側的客戶端之後,右側的客戶端得以連線到服務端,然後接收資料

  • 從上面我們可以看出,服務端沒有使用goroutine,因此其只能阻塞在處理客戶端的handleConn()函式中

使用goroutine

  • 如果想要讓服務端同時處理多個服務端,只需要做一點修改就可以了,那就是在服務端呼叫handleConn()函式之前加上go關鍵字,讓handleConn()函式在一個goroutine中進行執行,從而不阻塞主程式的執行
package main

import (
    "io"
    "log"
    "net"
    "time"
)

func main() {
    listener, err := net.Listen("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Print(err)
            continue
        }

        // 只修改了這個地方, 其他地方沒有任何修改
        go handleConn(conn)
    }
}

func handleConn(c net.Conn) {
    defer c.Close()
    for {
        _, err := io.WriteString(c, time.Now().Format("15:04:05\n"))
        if err != nil {
            return
        }
        time.Sleep(1 * time.Second)
    }
}
  • 將上面的服務端程式編譯好重新執行,如下:

 

  • 現在我們可以使用多個客戶端去連線服務端了,可以同時接收到資料,如下所示:

六、演示案例(併發回聲伺服器)

  • 待續

相關文章