进程:当运行一个应用程序时,操作系统会为这个应用程序(如运行了我们编译后的项目可执行文件)启动一个进程,维护程序的执行过程,为其进行资源分配和调度。所以一个应用程序对应一个进程。
线程:是进程执行中的一个实体,是其中的某一部分,是 CPU 调度和分配的基本单位,比进程更小且能独立运行。其受进程管理,一个进程可以创建和撤销多个线程,在同一个进程里的多个线程是可以并发执行的。
协程:是轻量级的线程,一个线程上可以跑多个协程,且可以并发执行。Go语言的协程是由编译器的运行时维护的,栈空间独立,堆空间共享。
并发与并行:通俗都会理解为同时执行多个任务。但针对与单核CPU来说,不存在多个任务真正意义上的同时执行,是用时间片轮换执行的原理,只有逻辑上的“同时”,这种叫并发;但是多核CPU上因为有多个物理计算核心,可以实现每个核心都同时各自工作,是真正提高了运行效率,这种叫并行。但是它们针对编程逻辑上,都有同样的结果表现,就是逻辑上可以按“同时”、“各自独立”运行来理解。更深层次的原理探讨在后面章节会有深入实践。
先看一段没有使用并发的源代码:
// conurrent project main.go
package main
import (
"fmt"
)
func timeOut(name string) {
num := 0
for i := 0; i < 5; i++ {
// 打印输出传进来的字符串和循环计数变量的值
fmt.Println(name, i)
// 用循环加法运算占用实践,没有用睡眠函数是因为其睡眠是没做任何事
for j := 0; j < 999999999; j++ {
num++
}
}
}
func main() {
timeOut("调用A")
timeOut("调用B")
fmt.Println("程序执行结束!")
}
上面代码编译运行后输出如下内容:
调用A 0
调用A 1
调用A 2
调用A 3
调用A 4
调用B 0
调用B 1
调用B 2
调用B 3
调用B 4
程序执行结束!
从运行结果可以看出,两次函数调用代码是顺序执行的。第23行调用 timeOut(“调用A”) 连续输出5次 “调用A” 后结束,才执行的第24行代码 timeOut(“调用B”),又连续输出5次 “调用B” 程序最后打印输出 “程序执行结束!” 后结束运行。
我们现在修改源代码,使用Go语言的并发运行方式,让两次函数调用并发运行,看是什么结果。先上修改后的源代码:
// conurrent project main.go
package main
import (
"fmt"
)
func timeOut(name string) {
num := 0
for i := 0; i < 5; i++ {
// 打印输出传进来的字符串和循环计数变量的值
fmt.Println(name, i)
// 用循环加法运算占用实践,没有用睡眠函数是因为其睡眠是没做任何事
for j := 0; j < 999999999; j++ {
num++
}
}
}
func main() {
// 调用函数前加 go 关键字,表示使用协程并发运行
go timeOut("调用A")
timeOut("调用B")
fmt.Println("程序执行结束!")
}
上面代码编译运行后输出如下内容:
调用B 0
调用A 0
调用A 1
调用B 1
调用A 2
调用B 2
调用A 3
调用B 3
调用A 4
调用B 4
程序执行结束!
从运行结果可以看出,两次函数调用代码是同时执行的。这就是并发运行,看着是不是很简单!对,这就是Go语言的并发之美,实现并发运行只需要一个 go 关键字就可以了。
第25行,是与之前代码唯一不同的地方,就是在调用函数之前加了一个 go 关键字,表示本次调用开辟一个协程去执行,与主流程代码并发运行。
上面代码中体验到了并发调用的简便高效的优势,那么各独立执行的协程之间的通信也会很简单吗?继续上代码:
// conurrent project main.go
package main
import (
"fmt"
)
func timeOut(ch chan int) {
num := 0
for i := 1; i < 5; i++ {
// 将循环变量的值发送到通道
ch <- i
// 用循环加法运算占用实践,没有用睡眠函数是因为其睡眠是没做任何事的
for j := 0; j < 999999999; j++ {
num++
}
}
// 循环结束向通道发送 0,为了让主协程的 for 循环可以跳出
ch <- 0
}
func main() {
ch := make(chan int)
// 调用函数前加 go 关键字,表示使用协程并发运行
go timeOut(ch)
for {
// 将数据通过channel投送给printer
num := <-ch
fmt.Println("收到的值:", num)
// 通道传过来 0 值,表示上面那个协程运行结束了
if num == 0 {
// 退出 for 循环
break
}
}
fmt.Println("程序执行结束!")
}
上面代码编译运行后输出如下内容:
收到的值: 1
收到的值: 2
收到的值: 3
收到的值: 4
收到的值: 0
程序执行结束!
从运行结果可以看出,第13行发送的循环变量值被依次打印输出,第22行的 0 值也被打印输出,而打印函数却没有在 timeOut() 的函数体内,而是在 main 函数中的第36行。其实是用的第27行定义的通道 ch 传输的。
第27行,声明一个用于传输整数类型的通道变量,通道是用于协程之间传输的数据用的 Go 语言专用类型。chan 是声明通道的关键字。
第30行,创建一个新协程运行 timeOut(ch) 函数,并将通道 ch 传进去。
第8行,定义一个函数 timeOut 函数,参数是通道类型。
第13行,循环体内依次将循环计数变量的值发送给通道,注意循环计数变量的值是从 1 起步的,如果还是 0 起步 main 函数里的第39行判断会在第一次循环的时候就成立了。<- 是向通道发送和从通道获取的标识符。
第22行,函数体代码运行到最后一行代码,需要通知 main 函数这里执行结束了,以使 main 函数可以继续向下运行,所以才有这行向通道发送一个 0 值做结束标志。
第35行,在无限 for 循环里,取出通道 ch 里的值给 num 变量。注意这里是会阻塞循环执行的,如果发送方已经发送了数据,这里可以取到,代码会继续执行。取完之后再次循环到这里,要有新的发送才会继续执行,否则要阻塞,因为之前发送的已经被取走了。是可以不阻塞的,后面章节再实践,这里重点先体验一下。
第39~43行,如果通道取到的值是 0 值,就跳出这个无限 for 循环,继续执行下面的代码。
第46行,执行完这行打印输出代码,整个程序就结束了。
以下是对本节涉及的 Go 语言编程内容的归纳总结,方便记忆:
● go,是并发运行启用一个新协程(goroutine)的关键字。在调用代码之前使用此关键字,后面调用的代码将是并发运行的。如:go abc() 表示 abc 函数将在一个新启用的协程里并发运行。
● chan,是声明定义 通道 的关键字。通道是各协程之间通信的桥梁,通道也是有数据类型的,在声明时要指定数据类型。更多类型相关后面章节会深入实践。
var ch chan int,表示声明一个传输 int 类型的通道 ch。这种格式声明后的通道是空值 nil,不能直接使用,需要配合 make 才可以使用,所以通常都用 make 来直接创建,而这种声明格式去掉 var 常出现在函数的参数中。
<-,是向通道发送数据或从通道读取数据的专用标识符。 ch <- 35 表示将 35 发送到通道 ch;v := <-ch,表示从通道 ch 取出数据赋值给变量 v。
.
.
上一节:Go/Golang语言学习实践[回顾]教程11–学习成绩统计的示例【下】