golang协程goroutines概念理解
goroutines
goroutine可以理解为go程,可以类比线程来理解,优势在于goroutine更加的轻量级。go在并发编程方面有其天然的优势。要使用go的多线程只需要在go关键词后面加函数或方法即可。
func loop() {
i:=0; i<10;i++ {
fmt.Println(i)
}
}
func main() {
go loop()
loop()
}
理论上上述代码会执行两遍,因为有两个线程并发执行,但是实际上打印结果只会输出一遍。原因在于main主线程执行完成就会退出,goroutine还没来得及执行。主线程退出后,goroutine也就没办法继续执行。我们先了解一下goroutine的是如何工作的。
go的并发与并行
go语言在逻辑处理器上调度goroutine,类比操作系统在物理处理器上调度线程。
并发指的是逻辑处理器根据任务执行的优先级自动暂停或者启动任务,比如一个任务执行到一半,先暂停后,执行完另一个任务后再回头继续执行这个任务。go语言对于并发的支持就在于他能高效的调度多个goroutine。
并行指的是同时执行多个任务。这就是需要多个逻辑处理器。
我们的关注点回到goroutine。goroutine之间是相互独立的,但是就像我们上面的例子,main主程跟go程之间没有沟通,导致go程还未执行,main主程就退出。因此goroutine之间就需要相互通信,这里就引入了信道概念(channel)。信道用<-进行赋值操作,箭头表示数据的流向。还可以用make创建
var ch chan int = make(chan int) 或
ch := make(chan int)
ch <- 0
这里我们创建的是非缓冲信道。信道流进流出需要成对出现,否则就会出现死锁的情况。什么是死锁。死锁指的是信道在数据流入后,就会阻塞。等待其他goroutine取出信道内的数据。如果不及时取出,就会一直阻塞从而造成死锁。
var ch := make(chan int)
func loop() {
i:=0;i<19;i++{
fmt.Println(i)
}
ch <- 0
}
func main(){
go loop()
fmt.Println(<-ch)
}
非缓冲信道不存储数据,只做数据的流入流出,因此,一下程序运行多次得到的结果可能都不相同。
var ch chan int = make(chan int)
func foo(id int) {
ch <- id
}
func main() {
for i := 0; i < 5; i++ {
go foo(i)
}
for i := 0; i < 5; i++ {
fmt.Println(<-ch)
}
}
缓冲信道
非缓冲信道不存储数据,可以理解为暂存数据,数据存入后阻塞等待goroutine取出,不取出就会造成死锁。有非缓冲信道就肯定会对应的缓冲信道,缓冲信道的定义与非缓冲信道的定义类似,区别在于缓冲信道会在类型后指定缓存的数据个数,例如 make(chan int, 3) 表示该信道可以存储三个数据,当存储的数据个数超过三个,会阻塞造成死锁
func main() {
ch := make(chan int, 3)
ch <- 0
ch <- 1
ch <- 2
fmt.Println(<-ch) // 0
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
}
缓冲信道遵循的是先进先出,可以把他看成一个队列。
WaitGroup
sync包是go的一个包,里面的WaitGroup types能够满足大部分low-level的routines使用,更高级别的goroutine同步还是需要用到channel信道来传递信息。下面贴一段 waitgroup的示例:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i:=1; i < 100; i++ {
fmt.Println("A:", i)
}
}()
go func() {
defer wg.Done()
for i := 1; i < 100 ; i++ {
fmt.Println("B:", i)
}
}()
wg.Wait()
}
参考连接:
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。