Goroutine
利用并行线程的操作,进程/线程的数量越多,切换成本越大,也就越浪费
一个线程分为用户空间与内核空间,将其分为线程(thread)与协程(co-routine),两者绑定。协程中又设计成N:1的关系,即一个协程调度器来调度N个协程。但是空间分布比还是1:1,协程的创建、删除和切换的代价都由CPU完成,消耗还是有点大。
最后将内核空间与用户空间做成M:N关系,也就是说内核空间中,不再是单一线程与单一协程绑定,而是多线程与协程调度器绑定,再联系到多个线程
协程co-routine在go中就改成了Goroutine,内存只有几kb,可以灵活调度。
老Go中的调度器有缺陷:1.创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争;2.M转移G会造成延迟和额外的系统负载;3.系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。
于是在新的Go中,利用了一个叫做GMP的东西,利用Process来更好的处理协调器以及应用线程。同时也有work stealing机制,从全局偷取,当M1的P在进行处理G1时,M2的P会利用这个机制从全局中偷G2放入本地序列中。所以Go是多进程的
在抢占过程中,co-routine是老的协程主动释放CPU,而goroutine则是最多10ms,立刻会被新的goroutine抢占CPU
创建goroutine方法
package main
import (
"fmt"
"time"
)
//从goroutine
func newTask() {
i := 0
for {
i++
fmt.Printf("new Goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
//主goroutine 如果主goroutine要结束,从goroutine要帮忙结束
func main() {
//创建一个go程,去执行newTask()流程
go newTask()
i := 0
for {
i++
fmt.Printf("main Goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
可以看出这里的异步发生属性:main Goroutine : i = 14;new Goroutine : i = 14;new Goroutine : i = 15;main Goroutine : i = 15;main Goroutine : i = 16;new Goroutine : i = 16;new Goroutine : i = 17;main Goroutine : i = 17
如果主goroutine要结束,从goroutine要帮忙结束,所以我们将main()中的循环关掉变成:
func main() {
go newTask()
fmt.Println("main goroutine exit")
}
这样的话打完"main goroutine exit"这行字就会结束,因为主goroutine要结束了。
或者直接在main()中构建方法:将进程分为有参数版和无参数版
package main
import (
"fmt"
//"runtime" //退出进程使用
"time"
)
func main() {
//无参版:用go创建承载一个形参为空,返回值为空的一个函数
go func() { //一个func()就为一个进程(function)
defer fmt.Println("A.defer")
//return //用return可以退出函数,但是只是退出当前函数,如果此函数为子函数则不能整个退出
func() {
defer fmt.Println("B.defer")
//runtime.Goexit() //可以退出所有父与子函数到下个函数
fmt.Println("B")
}() //这里没加小括号的话,只是函数定义,需要加小括号才能直接调用
fmt.Println("A")
}()
//有参版:
go func(a int, b int) bool {
fmt.Println("a =",a ,"b =", b)
return true
}(10, 20)
//为了让main不结束,需要加入一个死循环
for {
time.Sleep(1 * time.Second)
}
}
使用go func()来开启进程,也就是go程。在go中进程是异步进行的,在进程中可以使用return来退出当前进程,利用runtime.Goexit()则是退出所有进程。
Channal
但是在go程中,我们会发现goroutine的问题,就是协程的返回值不能通过return返回给主协程。于是乎就需要channel来进行go程中间的通信。
channal相当于是在两个goroutine中间作为通信的中介
通过通信来共享内存
channal的定义:
make(chan Type) //等价于make(chan Type, 0)
make(chan Type, capacity)
channal的使用:
channal <- value //发送value到channel
<-channel //接收并将其丢弃
x := <-channel //从channel中接收数据,并赋值给x
x, ok := <-channel //功能同上,同时检查通道是否已关闭或者是否为空
channal的实际应用:
package main
import "fmt"
func main() {
//定义一个channel
c := make(chan int)
go func() {
defer fmt.Println("goroutine结束")
fmt.Println("goroutine 正在运行...")
c <- 666 //将666 发送给c 是带阻塞的,必须要等到c把数据传过去
}()
num := <-c //从c中接收数据,并赋值给num 是带阻塞的,必须要等到c把数据传过来
fmt.Println("num =", num)
fmt.Println("main goroutine 结束...")
}
这里要注意:可以看到有父进程与子进程,两者是异步的,但是channel是有阻塞作用的。对于上面的程序也就是说,如果当子进程先一步到达c <- 666,那就会自动发送阻塞,需要等待父进程中num := <-c处理好才能真正结束,所以直到num取到了c的数据,defer才会打印"goroutine结束"。父进程先也是同理,如果先进行到了num,那也得先发生阻塞,必须等待c把数取到才能进行num取c的值。所以子进程中的defer fmt.Println("goroutine结束")永远会在num := <-c这行结束后运行。
有无缓冲channel的区别
1.无缓冲channel:必须要完成接棒这一步,一步都不能少,所以必须发送阻塞等待取值
程序与上面的例子一样
2.有缓冲channel:如果缓存空间内有物品,可以先取,不需要发送阻塞,但是如果取完了值也得发生阻塞等待放值。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 3) //带有缓冲的channel
fmt.Println("len(c) =", len(c), ",cap(c) =", cap(c))
go func() {
defer fmt.Println("子go程结束")
for i := 0; i < 4; i++ {
c <- i
fmt.Println("子go程正在运行,发送的元素为:", i,", len(c)=", len(c), ",cap(c)=", cap(c))
}
}()
time.Sleep(2 * time.Second)
for i := 0; i < 4; i++ {
num := <-c //从c中接收数据,并赋值给num
fmt.Println("num =", num)
}
fmt.Println("main 结束")
}
结果:
len(c) = 0 ,cap(c) = 3
子go程正在运行,发送的元素为: 0 , len(c)= 1 ,cap(c)= 3
子go程正在运行,发送的元素为: 1 , len(c)= 2 ,cap(c)= 3
子go程正在运行,发送的元素为: 2 , len(c)= 3 ,cap(c)= 3
num = 0
子go程正在运行,发送的元素为: 3 , len(c)= 3 ,cap(c)= 3
num = 1
num = 2
num = 3
main 结束
可以看出容量不够的时候会等待取出,取出完有空间了又继续放入,可以看出缓冲够的时候没有阻塞,但是缓冲不够的时候会进行阻塞。
关闭Channel
1.注意事项:
(1)channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显示的结束range循环之类的,才去关闭channel;
(2)关闭channel后,无法向channel再发送数据(引发panic错误后导致接收立即返回零值);
(3)关闭channel后,可以继续从channel接收数据;
(4)对于nil channel,无论收发都会被阻塞。
package main
import (
"fmt"
)
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//close可以关闭一个channel
close(c)
}()
for {
//ok如果为true表示channel没有关闭,如果为false表示channel已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
}else{
break
}
}
fmt.Println("Main Finished..")
}
在程序中可以体现出第三点,如果没有close(c)则会出现死锁问题,因为前面传入数据已经结束了,不会再传了。而让下面可以停止的操作则是关闭channel,这样一样可以读取,同时也不会继续要求上面的c再继续往里存东西。
nil channel则是不用make来初始化channel,这样是不行的,会导致出现nil channel然后收发都会被阻塞。
2.利用range来迭代不断操作channel(不需要再来判断ok,直接range自动读取里面的个数,但是close channel还是要的)
将上面的父级for循环换成如下代码
for data := range c {
fmt.Println(data)
}
fmt.Println("Main Finished..")
3.利用select来判断多路channel的监控状态功能
package main
import "fmt"
func fibonacii(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
//如果c可写,则该case就会进来
x = y
y = x + y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
//sub go
go func() {
for i := 0; i < 6; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
// main go
fibonacii(c, quit)
}
并发安全Lock
因为有通信来共享内存的机制,同时Go又有保存通过共享内存来实现通信的机制,所以这样就会存在多个Goroutine同时操作一块内存的情况,为保证数据的一致性,需要使用互斥锁进行并发控制。
同时在对文件、代码段、配置文件等数据进行访问时,需要进行排他访问,以保证数据的完整性。
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func addwithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
func Add() {
x = 0
for i := 0; i < 5, i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("WithoutLock",x)
x = 0
for i := 0; i < 5, i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock",x)
}
最后输出结果为不带锁发送计算错误,带锁则是正确的值,所以为了anti并发问题,对临界区进行控制,要加入锁。