详解Golang 中的并发限制与超时控制

栏目: 编程语言 · Go · 发布时间: 6年前

内容简介:这篇文章主要介绍了详解Golang 中的并发限制与超时控制,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

前言

上回在 用 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() 接受输入的参数,sleep 若干秒。然后通过 go 关键字并发执行,通过 channel 返回结果。

channel 顾名思义,他就是 goroutine 之间通信的“管道"。管道中的数据流通,实际上是 goroutine 之间的一种内存共享。我们通过他可以在 goroutine 之间交互数据。

ch <- xxx // 向 channel 写入数据
<- ch // 从 channel 中读取数据

channel 分为无缓冲(unbuffered)和缓冲(buffered)两种。例如刚才我们通过如下方式创建了一个无缓冲的 channel。

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 `分别 sleep 了 3,2,1秒。但总耗时只有 3 秒。所以并发生效了,go 的并发就是这么简单。

按序返回

刚才的示例中,我执行任务的顺序是 0,1,2。但是从 channel 中返回的顺序却是 2,1,0。这很好理解,因为 task 2 执行的最快嘛,所以先返回了进入了 channel,task 1 次之,task 0 最慢。

如果我们希望按照任务执行的顺序依次返回数据呢?可以通过一个 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.

超时控制

刚才的例子里我们没有考虑超时。然而如果某个 goroutine 运行时间太长了,那很肯定会拖累主 goroutine 被阻塞住,整个程序就挂起在那儿了。因此我们需要有超时的控制。

通常我们可以通过select + time.After 来进行超时检查,例如这样,我们增加一个函数 Run() ,在 Run() 中执行 go run() 。并通过 select + time.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 

这两者的区别在于,如果 channel 没有缓冲,或者缓冲区满了。goroutine 会自动阻塞,直到 channel 里的数据被读走为止。举个例子

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.

这是因为我们创建的 ch 是一个无缓冲的 channel。因此在执行到 ch<-"123",这个 goroutine 就阻塞了,后面的 fmt.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)
}

尽管读取了两次 channel,但是程序还是会死锁,因为缓冲区满了,goroutine 阻塞挂起。第二个 ch<- "123" 是没有办法写入的。

fatal error: all goroutines are asleep - deadlock!

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

Program exited.

因此,利用 channel 的缓冲设定,我们就可以来实现并发的限制。我们只要在执行并发的同时,往一个带有缓冲的 channel 里写入点东西(随便写啥,内容不重要)。让并发的 goroutine 在执行完成后把这个 channel 里的东西给读走。这样整个并发的数量就讲控制在这个 channel 的缓冲区大小上。

比如我们可以用一个 bool 类型的带缓冲 channel 作为并发限制的计数器。

chLimit := make(chan bool, 1)

然后在并发执行的地方,每创建一个新的 goroutine,都往 chLimit 里塞个东西。

  for i, sleeptime := range input {
    chs[i] = make(chan string, 1)
    chLimit <- true
    go limitFunc(chLimit, chs[i], i, sleeptime, timeout)
  }

这里通过 go 关键字并发执行的是新构造的函数。他在执行完原来的 Run() 后,会把 chLimit 的缓冲区里给消费掉一个。

  limitFunc := func(chLimit chan bool, ch chan string, task_id, sleeptime, timeout int) {
    Run(task_id, sleeptime, timeout, ch)
    <-chLimit
  }

这样一来,当创建的 goroutine 数量到达 chLimit 的缓冲区上限后。主 goroutine 就挂起阻塞了,直到这些 goroutine 执行完毕,消费掉了 chLimit 缓冲区中的数据,程序才会继续创建新的 goroutine。我们并发数量限制的目的也就达到了。

以下是完整代码

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 的缓冲是 1。task 0 和 task 1 耗时 2 秒超时。task 2 耗时 1 秒。总耗时 5 秒。并发限制生效了。

如果我们修改并发限制为 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)

还记得上面的例子么。如果 channel 不带缓冲,那么直到他被消费掉之前,这个 goroutine 都会被阻塞挂起。

然而如果这里的并发限制,也就是 chLimit 生效阻塞了主 goroutine,那么后面消费这些数据的代码并不会执行到。。。于是就 deadlock 拉!

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

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Linux Device Drivers

Linux Device Drivers

Jonathan Corbet、Alessandro Rubini、Greg Kroah-Hartman / O'Reilly Media / 2005-2-17 / USD 39.95

Device drivers literally drive everything you're interested in--disks, monitors, keyboards, modems--everything outside the computer chip and memory. And writing device drivers is one of the few areas ......一起来看看 《Linux Device Drivers》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试