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(工作线程),三者要互相绑定,才能正常运行。

image

信道 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