Go 并发

goroutine 是 Go 并发能力的核心要素。但是,goroutine 到底是什么?

叫做 goroutine 是因为已有的短语 — 线程、协程、进程等等 — 传递了不准确的含义。 goroutine 有简单的模型:它是与其他 goroutine 并行执行的,有着相同地址空间的函数。它是轻量的,仅比分配栈空间多一点点消耗。而初始时栈是很小的,所以它们也是廉价的,并且随着需要在堆空
间上分配(和释放)。

goroutine 是一个普通的函数,只是需要使用保留字 go 作为开头。

ready("Tea", 2) // 普通函数调用
go ready("Tea", 2)  // ready() 作为 goroutine 运行

看下面一个 goroutine 例子

func ready(w string, sec int) {
    time.Sleep(time.Duration(sec) * time.Second)
    fmt.Println(w, "is ready! ") 
}
func main() {
    go ready("Tea", 2)
    go ready("Coffee", 1)
    fmt.Println("I'm waiting")
    time.Sleep(5 * time.Second)
}

程序输出:
I’m waiting ← 立刻
Coffee is ready! ← 1 秒后
Tea is ready! ← 2 秒后

注意:主 goroutine 如果不等待内部启动的 goroutine 完成就终止,则任何正在执行的 goroutine 也都将停止。为了避免该情况,可以使用 channel 进行 goroutine 间通信。

channel

channel 可以与 Unix sehll 中的双向管道做类比:可以通过它发送或者接收值。channel 有三种:只发送,只接收,可同时发送、接收。

必须使用内置函数 make 创建 channel。

//只能发送的通道
csi := make(chan<- int)

//只能接收的通道
cri := make(<-chan int)

// 可同时发送和接收的通道
ci := make(chan int) // 用于发送和接收整数
cs := make(chan string) // 发送和接收 string
cf := make(chan interface{}) // 空接口满足各种类型

发送和接收数据是通过操作符 <- 完成的,根据其位置判断是发送还是接收数据。

ci<- 1 // 发送 1 到 channel ci
<-ci   // 从 ci 接收整数
i := <-ci // 从 ci 接收整数并赋值给 i

带缓冲的 channel

创建 channel 时也可以指定 channel 缓冲的大小,就是 channel 可以存储多少个元素。

ch := make(chan type, size)

size == 0 时,为不带缓冲的 channel,此时如果 ch 中有数据未读取,则再次向 ch 中发送数据时发送 goroutine 会被阻塞,直到其它 goroutine 从 ch 读取数据才能写入。当 size > 0 时,前 size 个元素可以无阻塞写入,当写入第 size + 1 个元素时,代码将被阻塞,直到其他 goroutine 从 ch 中读取一些元素,腾出空间为止。

range 和 close

使用 for val := range ch 能够不断的读取 channel 里面的数据,直到 channel 被显示的关闭。关闭 channel 之后就无法再发送任何数据了。

可以通过“逗号 ok”测试 channel 是否别关闭,

val, ok := <-ch

如果 ok 返回 false,则说明 channel 已经没有任何数据且已经被关闭。

注意: 记住应该让发送数据的一端关闭 channel,而不是接收的一端关闭它,这样容易引起 panic。不需要经常关闭 channel,只有确定没有任何数据发送或者想显示的结束 range 循环之类的时候才关闭它。

select

当有多个 channel 的时候,我们如何知道那个 channel 准备好?可以通过 select 监听 channel 上的数据流动。

select 默认是阻塞的,只有当监听的 channel 中有发送或接收可以进行时才会运行, 当多个 channel 都准备好的时候, select 是随机的选择一个执行的。

func fibonacci(c, quit chan int) {
     x, y := 1, 1
     for {
         select {
             case c <- x: // c 准备好
             x, y = y, x + y
             case <-quit: // quit 准备好
             fmt.Println("quit")
             return
         }
     }
}
func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

在select 里面有 default 语法, select 其实就是类似 switch 的功能, default 就是当监听的channel 都没有准备好的时候, 默认执行的( select 不再阻塞等待 channel)。

select {
    case i := <-c:
    // use i
    default:
    // 当 c 阻塞的时候执行这里
}

超时

有时候会出现 goroutine 阻塞的情况,那么我们如何避免整个的程序进入阻塞的情况呢?我们可以利用 select 来设置超时,通过如下的方式实现:

func main() {
    c := make(chan int)
    o := make(chan bool)
    go func() {
        for {
            select {
                case v := <- c:
                    println(v)
                case <- time.After(5 * time.Second): // 超时退出
                    println("timeout")
                    o <- true
                    break
            }
        }
    }()
    <- o
}

并行运行

虽然 goroutine 是并发执行的,但是它们并不是并行运行的。如果不告诉 Go 额外的东西,同一时刻只会有一个 goroutine 执行。利用 runtime.GOMAXPROCS(n) 可以设置 goroutine 并行执行的数量。

来自文档:

GOMAXPROCS 设置了同时运行的 CPU 的最大数量,并返回之前的设置。如果 n < 1,不会改变当前设置。当调度得到改进后,这将被移除。

如果不希望修改任何源代码,同样可以通过设置环境变量 OMAXPROCS 为目标值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值