这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
前面1-4的笔记仅仅是go语言语法基础。从本次笔记开始,我将记录更加进阶的语法、包等
本次笔记将记录并发协程的相关概念操作。
Goroutine
在介绍Goroutine
之前,需要引出以下几个概念
1.进程
进程是应用程序的启动实例,是系统进行资源分配和调度的基本单位,每个进程都有独立的内存空间,不同进程通过进程间的通信方式来通信。
2.线程
线程从属于进程,是进程中的一个实体,线程是CPU调度的基本单位,一个线程由线程ID、当前指令指针、寄存器集合和堆栈组成。
线程不拥有自己的系统资源,它与同属于同一进程的其他线程共享进程所拥有的全部资源,多个线程之间通过共享内存等线程间的通信方式来通信,线程拥有自己独立的栈和共享的堆。
3.协程
协程可以理解为轻量级线程,一个线程可以拥有多个协程。
与线程相比,协程不受操作系统调度,协程调度器按照调度策略把协程调度到线程中执行,协程调度器由应用程序的runtime包提供,用户使用go关键字即可创建协程,这也就是GO在语言层面直接支持协程的含义。
所以,标题的Goroutine
正是指上面三个之中的协程
,用户可以使用go关键字来创建协程
协程的优点:
1、使用协程与不使用协程对比:快,协程是一种并发机制,是一串运行在进程中的任务代码,只是这些任务代码可以交叉运行。
2、协程与进程线程对比:协程运行在用户态,能减少上下文切换带来的开销,并且协程调度器把可运行的协程逐个调度到线程中执行,同时及时把阻塞的协程调度出线程,从而有效的避免了线程的频繁切换,达到了使用少量的线程实现高并发的效果,但是对于一个线程来说每一时刻只能运行一个协程。
下面将通过代码直观的了解到协程
func hello(i int) {
println("hello Parallel : " + fmt.Sprint(i))
}
func Hello() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
代码目的:通过协程输出hello0-hello4
//go关键字后面需要跟随一个函数,可以是已定义的函数,也可以是如本例一样新构造一个一次性函数
输出结果如图:
由此可见,协程的运行是不分先后,可以说是随机的,同时也证明了协程的并发机制。这样做就会比单独for循环一个hello0-hello4更快,开销更低。
Channel
channel是指golang中的通道,它是用于携程中的通信,遵循先进先出,go不提倡通过共享内存来通信,而是提倡通过通信来实现内存共享,同时,channel是线程安全的。
以下为青训课的概念图
channel通过make关键字创建:intChan = make(chan int)
或intChan = make(chan int, 3)
,而这两个区别在于创建的channel是否有3个int类型的缓冲区。
向创建的channel写入数据:intChan<- 10
向创建的channel取出数据:num2 = <-intChan(先进先出)
//以下为示例
func CalSquare() {
println("this is channel")
src := make(chan int)
dest := make(chan int, 3)
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
//在此协程中得到10个数,并向channel src写入
go func() {
defer close(dest)
for i := range src {
dest <- int(math.Pow(float64(i), 2))
}
}()
//将写入src的数据取出,经过第二个协程做平方处理最后存入channel dest
for i := range dest {
println(i)
}
//遍历dest并打印dest中的数据
结果如图
Lock
众所周知,锁有排它锁(X)、享锁(S)以及意向锁(I)
本部分介绍一下go中如何使用锁,以及是否加锁对协程的区别
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
//加锁来计算x连续加2000次1
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
//不加锁来计算x连续加2000次1
func Lock() {
println("this is Lock")
x := 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("addWithLock:", x)
//执行5个协程,计算x+2000次1
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("addWithoutLock:", x)
//同上
}
//以上代码目的为:同上对x做5次连续加2000个1,结果应为10000
结果如图
加锁后执行得到了正确结果,不加锁的因读脏数据而导致结果不正确,比10000小
WaitGroup
sync.WaitGroup
用于并发操作中等待一组Goroutine的返回。首先需要告诉程序有几个并发操作sync.WaitGroup.Add(x)
,在结束某个协程后,可用sync.WaitGroup.Done()
来告诉程序结束了一个协程,最后利用sync.WaitGroup.Wait
来阻塞,直到所有协程结束后再释放。
归根结底,WaitGroup就是一个计数器,当add了x个协程后,计数器就由0增加到x。每次Done()一个,就相当于Add(-1)。Wait()是看计数器最后为0时释放,否则阻塞。
以下为代码样例
func waitHello(i int) {
println("hello Parallel : " + fmt.Sprint(i))
}
func WaitHello() {
println("this is waitGrop")
var wg sync.WaitGroup
//要并行执行5个协程,所以add(5)
wg.Add(5)
//for'循环5个协程
for i := 0; i < 5; i++ {
go func(j int) {
//执行完一个协程后通过defer来done
defer wg.Done()
waitHello(j)
}(i)
}
//阻塞并等待5个协程都结束才释放
wg.Wait()
}