1、goroutine 原理:
进程 - --> 一个线程 --->单线程程序
进程 - --> 多个线程 --->多线程程序
并发:多线程程序在一个核的CPU上运行
并行:多线程程序在多个核的CPU上运行
协程:独立的栈空间,共享堆空间,调度由自己控制, 本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。
Goroutine与线程的区别:
许多人认为goroutine比线程运行得更快,这是一个误解。Goroutine并不会更快,它只是增加了更多的并发性。当一个goroutine被阻塞(比如等待IO),golang的scheduler会调度其它可以执行的goroutine运行。与线程相比,它有以下几个优点:
内存消耗更少:
Goroutine所需要的内存通常只有2kb,而线程则需要1Mb(500倍)。
创建与销毁的开销更小
由于线程创建时需要向操作系统申请资源,并且在销毁时将资源归还,因此它的创建和销毁的开销比较大。相比之下,goroutine的创建和销毁是由go语言在运行时自己管理的,因此开销更低。
可以通过GOMAXPROCS()来设置并发度,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。
图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue)。
2、不同goroutine之间如何进行通讯?
a、全局变量和锁同步
b、channel
由于goroutine是异步执行的,那很有可能出现主程序退出时还有goroutine没有执行完,此时goroutine也会跟着退出。此时如果想等到所有goroutine任务执行完毕才退出,go提供了sync包和channel来解决同步问题,当然如果你能预测每个goroutine执行的时间,你还可以通过time.Sleep方式等待所有的groutine执行完成以后在退出程序。
A、主程序通过Wait阻塞,直到等待队列为0
B、通过channel实现goroutine之间的同步。
实现方式:通过channel能在多个groutine之间通讯,当一个goroutine完成时候向channel发送退出信号,等所有goroutine退出时候,利用for循环channe去channel中的信号,若取不到数据会阻塞原理,等待所有goroutine执行完毕,使用该方法有个前提是你已经知道了你启动了多少个goroutine。
3、channel:channel俗称管道,用于数据传递或数据共享,其本质是一个先进先出的队列,使用goroutine+channel进行数据通讯简单高效,同时也线程安全,多个goroutine可同时修改一个channel,不需要加锁。
a、类似Unix中的管道pip
b、先进先出(本质队列)
c、线程安全,多个goroutine同时访问,不需要加锁
d、channel是有类型的,整数channel只能存放整数
channel声明: var 变量名 chan 类型
channel必须通过make初始化, 不然无法赋值给管道。
示例: var name chan int
name = make(chan int , 10) //10为管道长度
name <- 10
channel可分为三种类型:
a、只读channel:只能读channel里面数据,不可写入
b、只写channel:只能写数据,不可读
c、一般channel:可读可写
4、发送者可以 close 一个 channel 来表示再没有值会被发送了。接收者可以通过赋值语句的第二参数来测试 channel 是否被关闭:当没有值可以接收并且 channel 已经被关闭,那么经过
v, ok := <-ch
之后 ok 会被设置为 `false`。
循环 `for i := range c` 会不断从 channel 接收值,直到它被关闭。
注意: 只有发送者才能关闭 channel,而不是接收者。向一个已经关闭的 channel 发送数据会引起 panic。
5、Go有一个语句叫做select
,用于监测各个信道的数据流动。
select 语句使得一个 goroutine 在多个通讯操作上等待。
select 会阻塞,直到条件分支中的某个可以继续执行,这时就会执行那个条件分支。当多个都准备好的时候,会随机选择一个。
当 select 中的其他条件分支都没有准备好的时候,`default` 分支会被执行。
为了非阻塞的发送或者接收,可使用 default 分支:
select {
case i := <-c:
// 使用 i
default:
// 从 c 读取会阻塞
}
goroutine、信道和死锁
信道是什么?简单说,是goroutine之间互相通讯的东西。
无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另一端已经准备好
1、从无缓冲信道取数据,必须要有数据流进来才可以,否则当前线阻塞数据流入无缓冲信道,
2、如果没有其他goroutine来拿走这个数据,那么当前线阻塞
如果信道正有数据在流动,我们还要加入数据,或者信道干涩,我们一直向无数据流入的空信道取数据呢? 就会引起死锁
func main() {
ch := make(chan int)
<- ch // 阻塞main goroutine, 信道c被锁
}
只有一个goroutine, 所以当你向里面加数据或者存数据的话,都会锁死信道, 并且阻塞当前 goroutine, 也就是所有的goroutine(其实就main线一个)都在等待信道的开放(没人拿走数据信道是不会开放的),也就是死锁咯。
只在单一的goroutine里操作无缓冲信道,一定死锁。
非缓冲信道上如果发生了流入无流出,或者流出无流入,也就导致了死锁。
func main() {
c := make(chan int)
go func() {
c <- 1
}()
}
程序正常退出了,很简单,并不是我们那个总结不起作用了,还是因为一个让人很囧的原因,main又没等待其它goroutine,自己先跑完了, 所以没有数据流入c信道,一共执行了一个goroutine, 并且没有发生阻塞,所以没有死锁错误。
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
for v := range ch {
fmt.Println(v)
}
}
range不等到信道关闭是不会结束读取的。也就是如果 缓冲信道干涸了,那么range就会阻塞当前goroutine, 所以死锁咯。
所以需要在range之前关闭通道close(ch)。
runtime调度器是个很神奇的东西,但是我真是但愿它不存在,我希望显式调度能更为自然些,多核处理默认开启。
关于runtime包几个函数:
Gosched
这个函数的作用是让当前goroutine让出CPU,好让其它的goroutine获得执行的机会。同时,当前的goroutine也会在未来的某个时间点继续运行。
NumCPU
返回当前系统的CPU核数量
GOMAXPROCS
设置最大的可同时使用的CPU核数
Goexit
退出当前goroutine(但是defer语句会照常执行)
当一个goroutine发生阻塞,Go会自动地把与该goroutine处于同一系统线程的其他goroutines转移到另一个系统线程上去,以使这些goroutines不阻塞
无缓冲的信道是一批数据一个一个的「流进流出」
缓冲信道则是一个一个存储,然后一起流出去。
gotroutine可能切换的点:
I/O,select、函数调用(有时),channel、runtime.Gosched()、等待锁。