Go语言并发学习目标
出色的并发性是Go语言的特色之一
- • 理解并发与并行
- • 理解进程和线程
- • 掌握Go语言中的Goroutine和channel
- • 掌握select分支语句
- • 掌握sync包的应用
并发与并行
并发与并行的概念这里不再赘述,
可以看看之前java版写的并发实践;
进程和线程
-
程序、进程与线程这里也不赘述
- 一个进程可以包括多个线程,线程是容器中的工作单位;
协程~Goroutine
概念:
协程(Coroutine),最初在1963年被提出,又称为微线程,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程,一个线程也可以拥有多个协程;
协程是编译器级的,进程和线程是操作系统级的。
协程不被操作系统内核管理,而完全由程序控制,因此没有线程切换的开销。和多线程比,线程数量越多,协程的性能优势就越明显。协程的最大优势在于其轻量级,可以轻松创建上万个而不会导致系统资源衰竭。
Go语言中的协程
Go与语言中的协程由运行时调度和管理,Go会智能将协程中的任务合理的分配给每个CPU;
一开始创建的协程堆栈很小,但是可以根据需要增长和收缩;
Coroutine与Goroutine
Goroutine能并行执行,Coroutine只能顺序执行,Go中Goroutine可以在单线程中产生,也可以在多线程中产生;
Coroutine程序需要主动交出控制权,系统才能获得控制权并将控制权交给其他Coroutine.
Coroutine属于协作处理,在应用程序不使用CPU时,需要让渡CPU,否则会使得计算机失去响应或者宕机;
Goroutine属于抢占式任务处理,和现有的多线程和多进程任务处理类似;应用程序对CPU的控制最终由操作系统来管理,如果操作系统发现一个应用程序长时间占用CPU,那么用户有权终止这个任务。
在Go中开启协程
在Go中开启协程----只需要在函数前面加上关键字go,将会同时运行一个新的Goroutine;
注意:使用go关键字创建协程时,被调用的函数往往没有返回值,如果函数有返回值,那么返回值会被忽略,那我们就是要返回值时,必须使用channel,通过channel把数据从中取出来;
Go程序的执行过程
创建和启动主Goroutine,初始化操作,执行main函数,当main函数执行结束后,程序也就结束了;
代码demo
func helloworld() {
fmt.Println("hello world")
}
func main() {
go helloworld()
fmt.Println("main exit")
}
运行结果:
但是这可能并不是我们看到的全部,如果main()的Goroutine比子Goroutine后终止,那么我们就会看到打印出的hello world;
多试几次(直接运行应该还是上面的结果,如果debug,就会出现打印hello world,这是因为协程有足够的时间反应;
我们来看一下如果加上睡眠时间:
func helloworld() {
fmt.Println("hello world")
}
func main() {
go helloworld()
time.Sleep(10 * time.Microsecond)
fmt.Println("main exit")
}
运行结果:
如果我们在fmt.Println("main exit")
前加上defer 会是什么结果?会打印 hello world吗?
我试了一下,不行:
我们来看一下defer 关键字的释义
“defer”语句调用一个函数,该函数的执行被延迟到周围的函数返回的那一刻,要么是因为周围的函数执行了return语句,到达了它的函数体的末尾,要么是因为对应的协程出现了恐慌。
所以下面的代码运行结果是什么?
代码demo3
func helloworld() {
fmt.Println("hello world")
}
func helloworld2() string {
fmt.Println("hello world222222")
return "hello world222222"
}
func main() {
go helloworld() //要留有足够的时间,否则main()的Goroutine终止了,程序将被终止,该协程根本就没有机会表现
defer helloworld2()
defer fmt.Println("main exit")
}
我们来分析一下 程序的执行(即代码demo3):
go程序启动时,runtime默认为main函数创建一个Goroutine;
在main函数的Goroutine执行到 go helloworld()即加了关键字go的方法时.归属于helloworld()函数的Goroutine被创建,helloworld()函数开始在自己的Goroutine中执行,
此时main函数的Goroutine继续执行,如果helloworld()函数不能够在main函数的Goroutine执行完毕之前将任务处理完毕,那么就会发生helloworld()函数没有执行的样子 ;
下面我们来修改上面的代码:
func helloworld() {
var i int
for {
i++
fmt.Println("add", i)
time.Sleep(time.Second)
}
}
func main() {
go helloworld() //要留有足够的时间,否则main()的Goroutine终止了,程序将被终止,该协程根本就没有机会表现
var str string
fmt.Scanln(&str) //阻塞
fmt.Println("main exit")
}
控制台不断输出add int,同时还可以接收用户输入。两个环节同时运行。
此时,main()继续执行,两个Goroutine通过Go程序的调度机制同时运行。
匿名函数创建Goroutine
即我们可以在匿名函数前加go关键字实现对匿名函数创建Goroutine;
func main() {
go func() {
var arr int
for {
arr++
fmt.Println("add", arr)
time.Sleep(time.Second)
}
}() //闭包
var str string
fmt.Scanln(&str)
fmt.Println("main exit")
}
启动多个Goroutine
func p1() {
for i := 0; i < 10; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Print(i)
}
}
func p2() {
for i := 'a'; i < 'i'; i++ {
time.Sleep(500 * time.Millisecond)
fmt.Printf("%c", i)
}
}
func main() {
go p1()
go p2()
var str string
fmt.Scanln(&str)
fmt.Println("main exit")
}
多个Goroutine随机调度,打印的结果是数字与字母交叉输出
并发性能优化
在Go程序运行时,go 关键字实现了一个小型的任务调度器,该调度器是使用CPU的,那么怎么为其分配CPU呢 ?
go中可以使用runtime.Gosched()来交出CPU的控制权,同时我们可以使用runtime.GOMAXPROCS()来匹配CPU核心数量
- Go1.5版本之前,默认使用单核执行。
- Go1.5版本开始,默认执行runtime.GOMAXPROCS(逻辑CPU数量),让代码并发执行,最大效率地利用CPU。
//Gosched生成处理器,允许其他goroutines运行。它不是挂起当前的goroutine,这个就像java中的yield(),只是让出cpu,但同时我还可以跟其他协程竞争cpu
func Gosched() {
checkTimeouts()
mcall(gosched_m)
}
// GOMAXPROCS设置可以执行的cpu的最大数目同时返回之前的设置。默认为 runtime.NumCPU的值。如果n < 1,则不改变当前设置。
//当调度器改进时,此调用将消失。
func GOMAXPROCS(n int) int {
if GOARCH == "wasm" && n > 1 {
n = 1 // WebAssembly has no threads yet, so only one CPU is possible.
}
lock(&sched.lock)
ret := int(gomaxprocs)
unlock(&sched.lock)
if n <= 0 || n == ret {
return ret
}
stopTheWorldGC("GOMAXPROCS")
// newprocs will be processed by startTheWorld
newprocs = int32(n)
startTheWorldGC()
return ret
}
Channel 通道
未完待续