本文主要是介绍Go Routine使用总结与并发使用同步方法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
GO对于高并发有着非常良好的支持能力,而GO实现高并发的两个主要工具便是Go Routine和Channel。这里主要对Go Routine进行介绍。
阅读后续之前请先意识到:不要用自己的想法推测编译器的执行顺序,编译器的执行顺序是完全不确定的!
基本概念
一般来说,协程就像轻量级的线程。
线程一般有固定的栈,有一个固定的大小。而goroutines为了避免资源浪费(亦或是资源缺乏),采用动态扩张收缩的策略:初始量为2k,最大可以扩张到1G。
另外,线程/goroutine 切换开销方面,goroutine 远比线程小
线程:涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP...等寄存器的刷新等。
因为协程在用户态由协程调度器完成,不需要陷入内核,所以goroutine只有三个寄存器的值修改 - PC / SP / DX,代价相对小很多
运行机制
go协程与一般的语言相比有很多有趣的特性,例如如下代码:
func hello() {fmt.Printf("Hello World!")
}func main() {go hello()
}
预期执行结果为输出“Hello World”,但结果为空
原因:函数执行期间启动go routine,函数并不会等待go routine运行完毕才退出,而是会直接退出
修改方式:对代码进行修改,增加main函数等待时间后即可看到正常结果
func hello() {fmt.Printf("Hello World!")
}func main() {go hello()time.Sleep(3)
}
当然,函数本身退出后子协程依然会继续运行直到运行结束或程序不在给他分配资源为止。上述案例无法看到现象是因为主函数会直接退出并使得系统清空全部分配的运行资源。
换为下面程序:
func hello() {go fmt.Printf("Hello World!\n")
}func main() {hello()fmt.Printf("hello function exit\n")time.Sleep(10)
}
会发现结果为:
即函数退出后子协程依然在正常运行
同步方法
接下来的文字主要是对Go官方文档关于Go Memory Model的说明
参考链接:https://golang.org/ref/memhttps://golang.org/ref/mem
If you must read the rest of this document to understand the behavior of your program, you are being too clever.
Don't be clever.
一段很有意思的话,个人理解是,写出正确的代码是重要的,不要过分纠结于炫技,也不要过分纠结于为何错误的代码是错误的。
Happens Before
首先正如当初学编译器提到的那样,很多时候我们写出来的代码的执行顺序并不是编译器编译的执行顺序。实际上很多时候,编译器会自动对语句的执行顺序进行调整优化,以更好利用寄存器信息。因此即使一开始写为a=1;b=1;,程序也可能先设置b再设置a。
所以,语句的执行顺序很多时候是不确定的,不要先入为主
主要同步机制
GO主要通过channel和锁对程序的执行顺序进行同步
1、Channel同步机制
var c = make(chan int)
var a stringfunc f() {a = "hello, world"c <- 0
}func main() {go f()<-cprint(a)
}
或者把channel的读入读出顺序进行更改
var c = make(chan int)
var a stringfunc f() {a = "hello, world"<-c
}func main() {go f()c <- 0print(a)
}
都可以保证得到预期的"hello, world"输出结果。这是因为channel没有缓存,必须读进去一个并立刻输出才行,否则会阻塞在对应语句处。
如果将channel换成有缓存的channel,例如
var c = make(chan int, 1)
var a stringfunc f() {a = "hello, world"<-c
}func main() {go f()c <- 0print(a)
}
就无法保证得到正确结果,因为channel可以将输入的0缓存后直接退出main函数
2、Lock同步机制
var l sync.Mutex
var a stringfunc f() {a = "hello, world"l.Unlock()
}func main() {l.Lock()go f()l.Lock()print(a)
}
For any call to
l.RLock
on async.RWMutex
variablel
, there is an n such that thel.RLock
happens (returns) after call n tol.Unlock
and the matchingl.RUnlock
happens before call n+1 tol.Lock
.
Go语言的设计机制保证了存在多对时,Unlock必定在Lock之后执行
如果在lock之前就执行unlock,将会得到如下结果:
func main() {mu.Unlock()
}fatal error: sync: unlock of unlocked mutex
程序直接报错
其他同步方法
1、Once的使用
var a string
var once sync.Oncefunc setup() {a = "hello, world"
}func doprint() {once.Do(setup)print(a)
}func twoprint() {go doprint()go doprint()
}
Once保证了一个函数只能执行一次
2、WaitGroup
func main() {wg := sync.WaitGroup{}wg.Add(100)for i := 0; i < 100; i++ {go f(i, &wg)}wg.Wait()
}// 一定要通过指针传值,不然进程会进入死锁状态
func f(i int, wg *sync.WaitGroup) { fmt.Println(i)wg.Done()
}
通过WaitGroup可以准确统计协程数量,以便在全部协程结束后才进行后续操作
必须注意的情况示例
var a string
var done boolfunc setup() {a = "hello, world"done = true
}func doprint() {if !done {once.Do(setup)}print(a)
}func twoprint() {go doprint()go doprint()
}
不要想当然按照自己的想法来理解编译器的习惯,很多时候执行顺序是不确定的。即使a在done前面,读到done的结果也不代表能读到a的结果。
Go并发编程范式
上面主要讲解了Go的一些编程范式
下面将介绍GO常见的一些并发写法
Wg的通常写法:启动协程时需要把参数一起进行传入
这样能得到正确结果,例如42315
如果启动协程时不采用go func(var){}(value)的形式,如下所示:
那么无法得到正确结果(例如45555)。
原因为:go func运行时读取的i的值已经进行了修改
这是一种很糟糕的设计,逻辑上是没有问题的,但是因为不断轮训导致CPU负载过高,单核CPU可能会迅速冲击到100%
解决方法:以毫秒为单位在for循环最后加一个时间等待函数
* 特别值得注意的一种语法:sync.NewCond
代码示例:
代码范式:
进一步提升了performance,避免出现了长时间不更新,每次轮询都是在空等待的情况出现几率
只有当cond进行了broadcast后才会唤起cond.Wait条件,进行后续处理
补充说明:这里值得注意的一个点是,cond里的锁是mu
初始声明:cond = sync.NewCond(&mu)
所以实际上cond里操作的锁就是mu,也因此才能实现后面的操作,打通两边的环节
具体说明可以参见下面的博客:
Golang sync.Cond详细理解_skh2015java的博客-CSDN博客_golang sync.cond
另外这里有需要担心的地方
理想情况是,先是进入wait for循环进入,然后wait和broadcast交互处理,节省CPU空转时间
但是有个问题,上面 broadcast的10个for循环连着跑完怎么办?
答:没影响,这样的话下面的for循环无法进入,直接加锁放锁而没有进入 wait的for循环,依然可以实现我们的目的
关于channel错误代码示例:
这个程序会直接堵住
程序串行执行,到第二步的时候直接堵死,无法走到第三行
channel在执行过程中需要有另一个go routine对channel进行处理,否则会堵住
用于让程序定量执行
例如:
写代码时经常一个不小心就陷入死锁,要注意规避死锁的情况出现,如下所示:
是否需要加锁、需要持有锁的时间等问题都需要进行斟酌
defer与defer func(){}的区别
核心在于对闭包的理解
func main() {var a = 1var b = 2defer fmt.Println(a + b)a = 2fmt.Println("main")
}
输出:
main
3
稍微修改一下,再看看:
func main() {var a = 1var b = 2defer func() {fmt.Println(a + b)}()a = 2fmt.Println("main")
}
输出:
main
4
结论:闭包获取变量相当于引用传递,而非值传递。
稍微再修改一下,再看看:
func main() {var a = 1var b = 2defer func(a int, b int) {fmt.Println(a + b)}(a, b)a = 2fmt.Println("main")
}
输出:
main
3
结论:传参是值复制。
还可以理解为:defer 调用的函数,参数的值在 defer 定义时就确定了,看下代码
defer fmt.Println(a + b),在这时,参数的值已经确定了。
而 defer 函数内部所使用的变量的值需要在这个函数运行时才确定,看下代码
defer func() { fmt.Println(a + b) }(),a 和 b 的值在函数运行时,才能确定。
参考资料
golang中协程和线程的区别 - 简书
使用 defer 函数 要注意的几个点_gr32442187do的博客-CSDN博客
这篇关于Go Routine使用总结与并发使用同步方法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!