A goroutine is a lightweight thread of execution.
goroutine是一个轻量执行线程。
使用go语句 start a new goroutine:
go 函数名( 参数列表 )
// 创建一个匿名函数的goroutine
go func (msg string) {
fmt.Println(msg)
} ("goroutine")
// 创建一个新goroutine调用某已定义函数
func f(from string) {
for i := 0; i < 3; i++ {
fmt.Println(from, ":", i)
}
}
go f("goroutine")
Golang创造出goroutine是为了很好地实现并发功能,因此了解goroutine的概念后要明白Go是如何用它实现并发并行的才能更好地使用它。
创建一个goroutine,存放在全局运行队列中。Go运行时的调度器将这些goroutine分配给一个逻辑处理器,并存放到其对应的本地运行队列中。逻辑处理器负责执行。这套管理、调度、执行goroutine的方式称为Go的并发。
咱们写一段并发代码表示得更清楚一些:
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup //计数信号量
func say(s string) {
defer wg.Done()
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond) //为了看到并发效果
fmt.Println(s)
}
}
func main() {
runtime.GOMAXPROCS(1) //只创建一个逻辑管理器
wg.Add(2)
go say("world") //start a goroutine
go say("hello") //start a goroutine
wg.Wait()
}
输出:
分析下这段代码:
写完代码大概知道go并发、goroutine怎么用了。至于其更深层次的原理(涉及go runtime等等),另外挖个坑慢慢填~
调度器同时分配全局运行队列中的goroutine到多个逻辑处理器上,称作Go的并行。
默认情况下,Go会给每个可用的物理处理器都分配一个逻辑处理器。比如电脑是4核的,则会默认创建4个逻辑处理器。这种情况下,若去掉上述代码中的 “runtime.GOMAXPROCS(1)”,则代码执行过程中其实是并发+并行的。
Channels are the pipes that connect concurrent goroutines.
通道是并发goroutine之间联系的管道,用来传递数据。
一个栗子:通过两个 goroutine 来计算数字之和
// make(chan val-type)
// Channels are typed by the values they convey.
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收
fmt.Println(x, y, x+y)
}
Channels provide a powerful way to reason about the flow of data from one goroutine to another without the use of locks or critical sections.
默认情况下,发送和接收会一直阻塞直到所有sender&receiver都准备好了。因此不需要另外用信号量控制,程序会自动等待消息收到。
默认通道是不带缓冲区的,此时发送方会阻塞直到接收方从通道中接收了值;
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小。此时发送方会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
func main() {
ch := make(chan int, 2) //设置缓冲区,大小为2
// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据,而不用立刻需要去同步读取数据
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
通道关闭后,不能再向通道发送信息,但是还可以接收。(A closed channel never blocks)
func main() {
ch := make(chan bool, 2)
ch <- true
ch <- true
close(ch)
for i := 0; i < cap(ch) +1 ; i++ {
v, ok := <- ch // ok返回量反映channel是否被关闭
fmt.Println(v, ok)
}
}
true true
true true
false false
使用range遍历通道:
func main() {
ch := make(chan bool, 2)
ch <- true
ch <- true
close(ch)
for v := range ch {
fmt.Println(v) // 被调用两次
}
}
select 语句使一个 goroutine 可以等待多个 channel 通信操作。
select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
// 起初case1可执行
case c <- x:
x, y = y, x+y
// 阻塞直到quit中有数据,取出,结束
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
// 先从c中取十次数据
// 阻塞直到c中有数据
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
// 再向quit中发送数据
quit <- 0
}()
fibonacci(c, quit)
}
为了在尝试发送或者接收时不发生阻塞,可使用 default 分支。当 select 中的其它分支都没有准备好时,default 分支就会执行。
再与WaitGroup结合:
const n = 100
finish := make(chan bool)
var done sync.WaitGroup
for i := 0; i < n; i++ {
done.Add(1)
go func() {
select {
case <-time.After(1 * time.Hour):
case <-finish:
}
done.Done()
}()
}
t0 := time.Now()
close(finish) // closing finish makes it ready to receive
done.Wait() // wait for all goroutines to stop
fmt.Printf("Waited %v for %d goroutines to stop\n", time.Since(t0), n)
将 channel 定义为 type chan struct{} ,表示 channel 没有任何数据;只对其关闭的特性感兴趣。在这种情况下使用:never send a value on the channel, and the receiver always discards any value received.
finish := make(chan struct{})
下一篇用go实现经典的并发模型:生产者消费者~
参考资料
https://www.flysnow.org/2017/04/11/go-in-action-go-goroutine.html
https://www.runoob.com/go/go-concurrent.html
https://www.jianshu.com/p/7f45d7989f3a
https://dave.cheney.net/2013/04/30/curious-channel