在上一篇幅中介绍了基础内容:
1、Go语言中关于函数定义、闭包函数、作用域、defer关键字详解
2、关于Go语言中的方法详解以及不提供继承机制的解决方案
3、关于Go语言中的接口与空接口解读
4、关于Go语言中的反射功能解读以及反射包reflect的相关API调用
前一篇中我们知道Go语言没有继承,通过struct和方法的组合来实现。
此外, Go语言作为一个静态语言,接口的出现让接口值呈现动态,可以在程序执行时候再确定动态类型和动态值。
并发编程
在CPU的早期,主要目标是提升处理器的处理频率。在提升频率遇到瓶颈后,CPU发展进入了多核时代。
编程语言也开始向并行化的模式发展,Go语言的并发是基于并行的。
并行是指同时运行多个线程,而并发是把一个任务拆分为多个小块去执行(本质上是时间轮片,轮番调度同一个处理器),不过在当前系统线程阻塞的情况下才会分配给其他核的线程。
主流的并行编程模式有多种,最为熟知的是多线程。多线程的并发可以自然地对应到多核处理器。
Go语言的多线程是基于消息传递的。Go语言将基于CSP(Communicating Sequential Process)
模型的并发编程内置到语言中,其特点就是goroutine
(协程)之间是共享内存的。
我们需要记住,Go语言最引人注目的是其高性能的并发编程,它是通过goroutine
(协程是可独立执行的单元)和channel
(通道是协程之间传递消息)实现的。
一、协程 goroutine
协程(goroutine
)是Go语言特有的一种轻量级线程,使用Go语言关键字启
动。goroutine
和系统线程是不一样的,不可等同来看。
实际上,所有的Go语言都是通过goroutine
运行的,我们所熟知的main函数
也是启用一个goroutine
来调用的。
1.1 协程概念
这里不在对进程、线程、协程进行概念的叙述,goroutine
比线程更为轻量,而线程比进程更为轻量。
一个进程可以有多个线程,每个线程可以有多个goroutine
;反过来goroutine
需要一个有进程的环境才可以运行。goroutine
运行的时候,需要有一个进程,并且进程至少有一个线程,进程和线程都由系统负责调度和管理,而Go语言工程师只需要负责goroutine
即可。
Go语言程序是通过调度程序组件使用m:n的调度技术来运行goroutine
的。m:n
是指多路复用n个操作系统线程执行m个goroutine
。
1.2 协程基本使用
启动一个协程,我们是使用go关键字去启动goroutine
。
func HelloWorld() {
fmt.Println("Hello,World!")
}
func main() {
go HelloWorld()
time.Sleep(1*time.Second)
fmt.Println("The End!")
}
运行的结果:
Hello,World!
The End!
这样看,大家可能觉得没有什么太大的问题,是顺序执行的。当我们把休眠这行代码注释掉,运行结果会发现:
The End!
这是因为,运行程序的时候,我们知道main函数其实也是一个goroutine
运行,当我们通过关键字go启动新的协程需要一定时间,此时main函数
会执行后面的代码,直接结束掉main函数所在的协程。
只要main函数结束,就会强制终止main函数程序中所有的goroutine
程序。
1.3 协程的执行过程
func main() {
go func() {
for i:=10;i<20;i++{
fmt.Print(" ",i)
}
}()
fmt.Println("====================================")
for i:=0;i<10;i++{
fmt.Print(" ",i)
}
time.Sleep(2*time.Second)
}
运行结果:
====================================================
10 11 12 13 14 15 16 17 18 19 0 1 2 3 4 5 6 7 8 9
main函数运行启动的goroutine
叫作主goroutine
,和关键字go
自启动goroutine
之间是并发执行的,执行过程中彼此没有关系。
1.4 启动多个协程
func main() {
for i:=0;i<20;i++{
go func(i int) {
fmt.Print(" ",i)
}(i)
}
time.Sleep(2*time.Second)
fmt.Println("\nThe End!")
}
运行结果:
1 18 2 4 3 11 5 6 13 7 8 14 9 10 15 17 16 19 0 12
The End!
循环语句中,通过匿名函数启动了20个goroutine
,这20个协程之间是并发执行的,不额外控制的话,顺序就是随机的。
存在休眠的目的是为了让协程的程序能执行完成,才会执行后面的语句。
1.5 sync包指定协程结束时间
var wg sync.WaitGroup
sync.WaitGroup
中state1
字段是一个计数器, 其用法也很好理解。每当有一个goroutine
运行的时候就调用Add方法给计数器加1,待一个goroutine
运行完后,通过Done
方法为计数器减1,然后使用Wait
方法等待计数器的数变为o。
Add()、Wait()方法是在主goroutine
中运行的,而Done()
方法是在自启动的goroutine
中运行的。
func main() {
var wg sync.WaitGroup
for i:=0;i<20;i++{
wg.Add(1) //每当有一个goroutine运行时候调用Add加1
go func(x int) {
defer wg.Done() //当有一个goroutine运行结束调用Done进行减1
fmt.Print(" ",x)
}(i)
}
fmt.Printf("\n%#v\n",wg)
wg.Wait() //使用Wait方法等待计数器的数变为0
fmt.Println("\nThe End!")
}
这里的关键字defer
存在,defer关键字后面跟函数调用语句,defer的触发机制包含下面三种情况:
这里我们需要注意调用时机(偏向于延迟调用):
- defer所在的函数返回时,触发defer后面的函数调用语句。
- defer所在的函数执行到末尾时,触发defer后面的函数调用语句。
- defer所在的函数报panic时, 触发defer后面的函数调用语句。
注意:当我们将 wg
传入到方法内的时候,我们需要传递的是地址而不能是变量,因为值传递过程中,地址作为参数传入,会与方法作用域外一致。而传入的是变量,比如 ( wg sync.WaitGroup
)则会产生新的变量与作用域外不一致,导致死锁问题出现。