前言

上回在 用 Go 写一个轻量级的 ssh 批量操作工具 里提及过,我们做 Golang 并发的时候要对并发进行限制,对 goroutine 的执行要有超时控制。那会没有细说,这里展开讨论一下。

以下示例代码全部可以直接在 The Go Playground 上运行测试:

并发

我们先来跑一个简单的并发看看

package main

import (
    "fmt"
    "time"
)

func run(task_id, sleeptime int, ch chan string) {

    time.Sleep(time.Duration(sleeptime) * time.Second)
    ch <- fmt.Sprintf("task id %d , sleep %d second", task_id, sleeptime)
    return
}

func main() {
    input := []int{3, 2, 1}
    ch := make(chan string)
    startTime := time.Now()
    fmt.Println("Multirun start")
    for i, sleeptime := range input {
        go run(i, sleeptime, ch)
    }

    for range input {
        fmt.Println(<-ch)
    }

    endTime := time.Now()
    fmt.Printf("Multissh finished. Process time %s. Number of tasks is %d", endTime.Sub(startTime), len(input))
}
run()sleepgochannel
channelgoroutinegoroutinegoroutine
ch <- xxx // 向 channel 写入数据
<- ch // 从 channel 中读取数据
channelchannel
ch := make(chan string)
channel
Multirun start
task id 2 , sleep 1 second
task id 1 , sleep 2 second
task id 0 , sleep 3 second
Multissh finished. Process time 3s. Number of tasks is 3
Program exited.
goroutine

按序返回

channelchannel
channel
package main

import (
    "fmt"
    "time"
)

func run(task_id, sleeptime int, ch chan string) {

    time.Sleep(time.Duration(sleeptime) * time.Second)
    ch <- fmt.Sprintf("task id %d , sleep %d second", task_id, sleeptime)
    return
}

func main() {
    input := []int{3, 2, 1}
    chs := make([]chan string, len(input))
    startTime := time.Now()
    fmt.Println("Multirun start")
    for i, sleeptime := range input {
        chs[i] = make(chan string)
        go run(i, sleeptime, chs[i])
    }

    for _, ch := range chs {
        fmt.Println(<-ch)
    }

    endTime := time.Now()
    fmt.Printf("Multissh finished. Process time %s. Number of tasks is %d", endTime.Sub(startTime), len(input))
}

运行结果,现在输出的次序和输入的次序一致了。

Multirun start
task id 0 , sleep 3 second
task id 1 , sleep 2 second
task id 2 , sleep 1 second
Multissh finished. Process time 3s. Number of tasks is 3
Program exited.

超时控制

goroutinegoroutine
selecttime.AfterRun()Run()go run()selecttime.After
package main

import (
    "fmt"
    "time"
)

func Run(task_id, sleeptime, timeout int, ch chan string) {
    ch_run := make(chan string)
    go run(task_id, sleeptime, ch_run)
    select {
    case re := <-ch_run:
        ch <- re
    case <-time.After(time.Duration(timeout) * time.Second):
        re := fmt.Sprintf("task id %d , timeout", task_id)
        ch <- re
    }
}

func run(task_id, sleeptime int, ch chan string) {

    time.Sleep(time.Duration(sleeptime) * time.Second)
    ch <- fmt.Sprintf("task id %d , sleep %d second", task_id, sleeptime)
    return
}

func main() {
    input := []int{3, 2, 1}
    timeout := 2
    chs := make([]chan string, len(input))
    startTime := time.Now()
    fmt.Println("Multirun start")
    for i, sleeptime := range input {
        chs[i] = make(chan string)
        go Run(i, sleeptime, timeout, chs[i])
    }

    for _, ch := range chs {
        fmt.Println(<-ch)
    }
    endTime := time.Now()
    fmt.Printf("Multissh finished. Process time %s. Number of task is %d", endTime.Sub(startTime), len(input))
}

运行结果,task 0 和 task 1 已然超时

Multirun start
task id 0 , timeout
task id 1 , timeout
tasi id 2 , sleep 1 second
Multissh finished. Process time 2s. Number of task is 3
Program exited.

并发限制

goroutine
channel
channel
ch := make(chan string) // 这是一个无缓冲的 channel,或者说缓冲区长度是 0
ch := make(chan string, 1) // 这是一个带缓冲的 channel, 缓冲区长度是 1 
channelgoroutinechannel
package main

import (
    "fmt"
)

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

这段代码执行将报错

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /tmp/sandbox531498664/main.go:9 +0x60

Program exited.
chchannelch<-"123"goroutinefmt.Println(<-ch)deadlock

如果我们改成这样,程序就可以执行

package main

import (
    "fmt"
)

func main() {
    ch := make(chan string, 1)
    ch <- "123"
    fmt.Println(<-ch)
}

执行

123

Program exited.

如果我们改成这样

package main

import (
    "fmt"
)

func main() {
    ch := make(chan string, 1)
    ch <- "123"
    ch <- "123"
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
goroutinech<- "123"
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /tmp/sandbox642690323/main.go:10 +0x80

Program exited.
channelgoroutinechannelchannel
boolchannel
    chLimit := make(chan bool, 1)
chLimit
    for i, sleeptime := range input {
        chs[i] = make(chan string, 1)
        chLimit <- true
        go limitFunc(chLimit, chs[i], i, sleeptime, timeout)
    }
goRun()chLimit
    limitFunc := func(chLimit chan bool, ch chan string, task_id, sleeptime, timeout int) {
        Run(task_id, sleeptime, timeout, ch)
        <-chLimit
    }
goroutinechLimitgoroutinegoroutinechLimitgoroutine

以下是完整代码

package main

import (
    "fmt"
    "time"
)

func Run(task_id, sleeptime, timeout int, ch chan string) {
    ch_run := make(chan string)
    go run(task_id, sleeptime, ch_run)
    select {
    case re := <-ch_run:
        ch <- re
    case <-time.After(time.Duration(timeout) * time.Second):
        re := fmt.Sprintf("task id %d , timeout", task_id)
        ch <- re
    }
}

func run(task_id, sleeptime int, ch chan string) {

    time.Sleep(time.Duration(sleeptime) * time.Second)
    ch <- fmt.Sprintf("task id %d , sleep %d second", task_id, sleeptime)
    return
}

func main() {
    input := []int{3, 2, 1}
    timeout := 2
    chLimit := make(chan bool, 1)
    chs := make([]chan string, len(input))
    limitFunc := func(chLimit chan bool, ch chan string, task_id, sleeptime, timeout int) {
        Run(task_id, sleeptime, timeout, ch)
        <-chLimit
    }
    startTime := time.Now()
    fmt.Println("Multirun start")
    for i, sleeptime := range input {
        chs[i] = make(chan string, 1)
        chLimit <- true
        go limitFunc(chLimit, chs[i], i, sleeptime, timeout)
    }

    for _, ch := range chs {
        fmt.Println(<-ch)
    }
    endTime := time.Now()
    fmt.Printf("Multissh finished. Process time %s. Number of task is %d", endTime.Sub(startTime), len(input))
}

运行结果

Multirun start
task id 0 , timeout
task id 1 , timeout
task id 2 , sleep 1 second
Multissh finished. Process time 5s. Number of task is 3
Program exited.
chLimit

如果我们修改并发限制为 2

chLimit := make(chan bool, 2)

运行结果

Multirun start
task id 0 , timeout
task id 1 , timeout
task id 2 , sleep 1 second
Multissh finished. Process time 3s. Number of task is 3
Program exited.

task 0 , task 1 并发执行,耗时 2秒。task 2 耗时 1秒。总耗时 3 秒。符合预期。

channel
chs[i] = make(chan string, 1)
channelgoroutinechLimitgoroutinedeadlock
    for _, ch := range chs {
        fmt.Println(<-ch)
    }

所以给他一个缓冲就好了。

参考文献

从Deadlock报错理解Go channel机制(一)
golang-what-is-channel-buffer-size
golang-using-timeouts-with-channels

以上

转载授权