25.goroutine
25.goroutine
1,前言
1,并行和并发
- 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。
- 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
- 并行是两个队列
同时
使用两台咖啡机 - 并发是两个队列
交替
使用一台咖啡机
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。 并行的关键是你有同时处理多个任务的能力。 所以我认为它们最关键的点就是:是否是**『同时』**。
2,Go语言并发的优势
有人把Go比作21世纪的C语言,第一是因为Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而Go从语言层面就支持了并发。同时,并发程序的内存管理有时候是非常复杂的,而Go语言提供了自动垃圾回收机制。
Go语言为并发编程而内置的上层API基于CSP(communicating sequential processes, 顺序通信进程)模型。这就意味着显式锁都是可以避免的,因为Go语言通过相册安全的通道发送和接受数据以实现同步,这大大地简化了并发程序的编写。
一般情况下,一个普通的桌面计算机跑十几二十个线程就有点负载过大了,但是同样这台机器却可以轻松地让成百上千甚至过万个goroutine进行资源竞争。
2,goroutine
1,goroutine是什么
goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
2,创建goroutine
只需在函数调⽤语句前添加 go 关键字,就可创建并发执⾏单元。开发⼈员无需了解任何执⾏细节,调度器会自动将其安排到合适的系统线程上执行。
在并发编程里,我们通常想讲一个过程切分成几块,然后让每个goroutine各自负责一块工作。当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。
示例:
package main
import (
"fmt"
"time"
)
func Newtask() {
for {
fmt.Println("this is a Newtask")
time.Sleep(time.Second) //表示延时1s
}
}
func main() {
go Newtask() //新建一个协程,一个任务
for {
fmt.Println("this is a main goroutine")
time.Sleep(time.Second) //表示延时1s
}
}
3,主协程退出,子协程也会退出
package main
import (
"fmt"
"time"
)
//如果主协程退出了,其他子协程也会跟着退出
func main() {
go func() {
i := 0
for {
i++
fmt.Println("子协程 i = ", i)
time.Sleep(time.Second)
}
}() //别忘了()进行调用
i := 0
for {
i++
fmt.Println("main i = ", i)
time.Sleep(time.Second)
if i == 2 {
break
}
}
}
所以有时候会出现一种情况,就是程序运行之后什么都没有输出,有可能就是子协程还没来得及打印的时候,主协程就已经退出了。要注意这个点。
4,runtime包
1,Gosched
runtime.Gosched() 用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
这就像跑接力赛,A跑了一会碰到代码runtime.Gosched() 就把接力棒交给B了,A歇着了,B继续跑。
示例代码:
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
for i := 0; i < 5; i++ {
fmt.Println("go")
}
}()
for i := 0; i < 2; i++ {
//表示让出时间片,别的协程执行完成之后,再来执行主协程
runtime.Gosched()
fmt.Println("hello")
}
}
输出结果:
$ go run 4_Gosched的使用.go
go
go
go
go
go
hello
hello
如果不加runtime.Gosched()
函数,那么结果会是如下:
$ go run 4_Gosched的使用.go
go
hello
go
hello
2,Goexit
调用 runtime.Goexit() 将立即终止当前 goroutine 执⾏,调度器确保所有已注册 defer 延迟调用被执行。
package main
import (
"fmt"
"runtime"
)
func test() {
defer fmt.Println("ccc")
//return //表示终止次函数
runtime.Goexit() //表示终止所在的协程
fmt.Println("ddd")
}
func main() {
//创建新的协程
go func() {
fmt.Println("aaa")
//调用别的函数
test()
fmt.Println("bbb")
}()
//特地写一个死循环,目的不让主协程结束
for {
}
}
3,GOMAXPROCS
调用 runtime.GOMAXPROCS() 用来设置可以并行计算的CPU核数的最大值,并返回之前的值。
package main
import (
"fmt"
"runtime"
)
func main() {
n := runtime.GOMAXPROCS(4) //指定以单核运算
fmt.Println("n = ", n)
for {
go fmt.Print(1)
fmt.Print(0)
}
}
在第一次执行(runtime.GOMAXPROCS(1))时,最多同时只能有一个goroutine被执行。所以 会打印很多1。过了一段时间后,GO调度器会将其置为休眠,并唤醒另一个goroutine,这时候就开始打印很多0了,在打印的时候,goroutine是被调度到操作系统线程上的。
在第二次执行(runtime.GOMAXPROCS(2))时,我们使用了两个CPU,所以两个goroutine可以一起被执行,以同样的频率交替打印0和1。
4,sync.WaitGroup
还有一种方式是利用sync包内的一个方法来实现:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
func f1(i int) {
defer wg.Done() // goroutine结束就登记-1
time.Sleep(time.Second * time.Duration(rand.Intn(3)))
fmt.Println(i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) //启动一个goroutine就登记+1
go f1(i)
}
wg.Wait() //等待所有等级的goroutine都结束
}
5,多协程导致资源竞争问题
package main
import (
"fmt"
"time"
)
//定义一个打印机,参数为字符串,按每个字符串打印
//打印机属于公共资源
func Printer(str string) {
for _, data := range str {
fmt.Printf("%c", data)
time.Sleep(time.Second)
}
fmt.Printf("\n")
}
func person1() {
Printer("hello")
}
func person2() {
Printer("world")
}
func main() {
//新建2个协程,代表2个人,2个人同时使用打印机
go person1()
go person2()
//特地不让主协程结束,创建一个死循环
for {
}
}
输出如下结果:
$ go run 7_多任务导致资源竞争问题.go
whoerllldo
exit status 2