Go语言并发
在go语言中,程序是以goroutine的形式执行的,每个goroutine就是一个轻量级用户线程,可以使用关键字go来说生成一个新的线程去执行函数,类似于创建thread时指定它要执行的函数:
func test_go() {
fmt.Println("helloworld")
}
func main() {
go test_go()
time.Sleep(time.Second)
}
//output
helloworld
GPM模型
每个 P 都有一个队列,用来存正在执行的 G。避免 Global Sched Lock。
每个 M 运行都需要一个 MCache 结构。M Pool 中通常有较多 M,但执行的只有几个,为每个池子中的每个 M 分配一个 MCache 则会形成不必要的浪费,通过把 cache 从 M 移到 P,每个运行的 M 都有关联的 P,这样只有运行的 M 才有自己的 MCache。
Goroutine的增长是在堆上分配空间的。
https://zhuanlan.zhihu.com/p/323271088
https://www.zhihu.com/question/20862617
教程:https://www.bilibili.com/video/BV19r4y1w7Nx
G(goroutine),P(processor),M(工作线程),三者要互相绑定,才能正常运行。

信道 chan
信道是一个先进先出的数据结构,可以看作一个线程安全的队列。
go语言中,每个goroutine可以通过channel变量进行通信,来感知别的线程的工作情况。
channel变量在不指定缓冲区大小的情况下是无缓冲的
make(chan int , 2) 返回的是一个缓冲区大小为2的信道,这个返回值作用像是一个指向底层数据结构的引用。所以在函数间进行作为参数进行传递就跟传引用一样,都引用/使用这个底层信道数据对象。
c:=make(chan int) //创建一个信道内传递整数的无缓冲信道
//在函数外声明chan变量
var c chan int=make(chan int)
对于无缓冲的信道,对信道的读和写都是阻塞的。无缓冲的信道不做数据的存储,只体现为数据的流通。
1.对于写而言,如果没有goroutine从信道读取,则写的goroutine会被阻塞挂起
写的语法:channel<-5
2.对于读而言,如果没有goroutine对信道写入,则读的goroutine会被阻塞挂起
读的语法:<-channel
var channel chan int = make(chan int) //必须要make赋值,否则是个nil的chan,行为会异常
func test_go() {
channel <- 5
fmt.Println("helloworld")
}
func main() {
go test_go()
<-channel
time.Sleep(time.Second)
}
//output helloworld
因此,对于无缓冲的信道,读写必须是成套的,在没有成套之前,goroutine会被挂起,所以这里也很容易出现死锁问题。
go语言在运行时能感知到信道的死锁,若出现死锁,运行时报错:
fatal error: all goroutines are asleep - deadlock!
缓冲信道,在使用make创建信道时,可以指定信道的大小,也即缓冲区的大小。这时的信道有点像容器一样,可以装最多缓冲区大小的元素。
c2:=make(chan int,2) //创建一个信道内传递整数的缓冲去大小为2的信道
//在函数外声明chan变量
var c2 chan int=make(chan int,2)
若信道缓冲区已满,后续对信道的写入将会阻塞,goroutine会被挂起;同理,如果信道缓冲区为空,则读取操作讲阻塞挂起。带缓冲的信道就和TCP连接的独写处理方式相似了。
控制信道的读写权限:
var c1 chan int //可读可写
var c2 <-chan int //只读
var c3 chan<- int //只写
信道的读取和关闭
带缓冲的信道如果某一时刻有很多个值在里面,逐一使用<-channel的方式来读取代码上重复累赘,使用range可以遍历信道的元素(遍历操作就是读取),但如果信道是没有关闭的(即还能往信道里写入值),那么range的遍历是不会终止的,它会一直等下一个元素的到来,此时也会产生死锁。
var channel chan string = make(chan string, 1)
func test_go() {
channel <- "hello"
fmt.Println("helloworld")
}
func main() {
go test_go()
for ele := range channel {
fmt.Println(ele)
}
time.Sleep(time.Second)
}
//output
helloworld
hello
fatal error: all goroutines are asleep - deadlock!
要么就是在for循环内添加if判断,判断信道当前大小len(channel)是否为0,是的话则break,但这种方法一定要保证没有其他的goroutine同时往信道写入,否则用于不会break。
另外一种方式就是关闭信道,close(channel),关闭信道只是关闭了写入了,仍可正常读取,和TCP的close有点相像。
var channel chan string = make(chan string, 1)
func test_go() {
channel <- "hello"
close(channel)
fmt.Println("helloworld")
}
func main() {
go test_go()
time.Sleep(time.Second)
for ele := range channel {
fmt.Println(ele)
}
}
//output
helloworld
hello
或者通过val,ok:=<-channel的方式来遍历也可以,如果信道关闭了则ok为false,停止循环:
for {
if val, ok := <-channel; ok {
fmt.Println(val)
} else {
break
}
}
向一个关闭了信道继续写数据会引起程序panic
信道作为函数参数传递
不论是值传递还是传指针,效果跟传引用一样,修改了传入的信道:
//example 1
func test_trans(c *chan string) {
*c <- "world"
close(*c)
}
func main() {
test_trans(&channel)
time.Sleep(time.Second)
for {
if val, ok := <-channel; ok {
fmt.Println(val)
} else {
break
}
}
fmt.Println(len(channel))
} //output world 0
//example 2
func test_trans(c chan string) {
c <- "world"
close(c)
}
func main() {
test_trans(channel)
time.Sleep(time.Second)
for {
if val, ok := <-channel; ok {
fmt.Println(val)
} else {
break
}
}
fmt.Println(len(channel))
} //output world 0
channel底层的数据结构,多个goroutine同时访问一个channel是协程安全的,因为底层数据结构有一个mutex互斥量,以保证每次从channel的写入和取出操作都是互斥的,原子性的,所以可以放心使用,效率和共享的队列应该差不多,毕竟都是用mutex来控制。
type hchan struct {
// 通道里元素的数量
qcount uint
// 循环队列的长度
dataqsiz uint
// 指针,指向存储缓冲通道数据的循环队列
buf unsafe.Pointer
// 通道中元素的大小
elemsize uint16
// 通道是否关闭的标志
closed uint32
// 通道中元素的类型
elemtype *_type
// 已接收元素在循环队列的索引
sendx uint
// 已发送元素在循环队列的索引
recvx uint
// 等待接收的协程队列
recvq waitq
// 等待发送的协程队列
sendq waitq
// 互斥锁,保护hchan的并发读写,下文会讲
lock mutex
}
————————————————
原文链接:https://blog.csdn.net/jiangjiang04/article/details/105481940/
sync.Mutex
虽然使用chan变量可以实现不同goroutine之间的通信和同步,但有时候我们并不需要goroutine之间通信,而需要他们在访问同一共享对象时互斥,这时候就可以使用Go标准库提供的sync.Mutex互斥锁,原理上和C++的mutex是一样的,同一时刻只有一个goroutine获得了锁标记。Mutex在Go标准库sync包中。
import "sync"
type HasLock struct{
mutex sync.Mutex
}
var global int = 20
func test_mutex(hl *HasLock){
hl.mutex.Lock()
fmt.Println(global++)
hl.mutex.UnLock()
}
func main(){
local_mutex:=HasLock{}
for i:=0;i<10;i++{
go test_mutex(&local_mutex)
}
time.Sleep(time.Second*5)
}
//output
21
22
23
24
25
26
27
28
29
30
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
process(req) // Buggy; see explanation below.
<-sem
}()
}
}
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func(req *Request) {
process(req)
<-sem
}(req)
}
}
注意上述两块代码,第一块代码中存在bug,原因是每个goroutine都复用了for循环中的局部变量req,共享了req(官网是这样解释的, 但是个人觉得好像没影响,感觉每次循环的req的值都不一样,应该不存在共享变量的问题);第二块代码则是通过闭包的方式,每个goroutine都有自己req的副本,不存在共享问题,也可像下面这样:
func Serve(queue chan *Request) {
for req := range queue {
req := req // Create new instance of req for the goroutine.
sem <- 1
go func() {
process(req)
<-sem
}()
}
}
注意比较奇怪的语法:在循环体内部用了req:=req,虽然看起来奇怪,但是go语言支持。
You get a fresh version of the variable with the same name, deliberately shadowing the loop variable locally but unique to each goroutine.
获取电脑的CPU核数:runtime.NumCPU()
获取配置文件中的GOMAXPROCS参数:runtime.GOMAXPROCS(0)
can