本文结构:
1、概念
2、goroutine
3、goroutine调度模型
4、不同goroutine之间通讯方式
5、goroutine之间的通信实例
6、select多路复用
1、概念:
进程和线程
1)进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位
2)线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位
3)一个进程能创建和撤销多个线程;同一进程中多个线程之间可以并发执行
并发和并行
1)多线程程序在一个核的CPU上运行,就是并发
2)多线程程序在多个核的cpu上运行,就是并行
协程和线程
1)协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上类似于用户级线程,这些用户级线程的调度也是自己实现(goroutine)
2)线程:一个线程上可以跑多个协程,协程是轻量级的线程
2、goroutine
在go语言中,每一个并发的执行单元叫做一个goroutine。当一个程序执行时,只有一个goroutine来调用main函数,这个routine称为主goroutine,新的goroutine通过关键字 go 进行创建—在函数/方法调用前加上go。
简单goroutine练习:
package main
import (
"fmt"
"time"
)
func test() {
var i int
for {
fmt.Println(i)
time.Sleep(time.Second)
i++
}
}
func main() {
go test()
}
当运行程序,不会输出任何结果,因为主线程退出,协程没有机会执行,简单改进(在4goroutine通信会通过一个管道判断goroutine什么时候退出的):
package main
import (
"fmt"
"time"
)
func test(){
var i int
for{
fmt.Println(i)
time.Sleep(time.Second)
i++
}
}
func main(){
go test()
for {
fmt.Println("i running in main")
time.Sleep(time.Second)
}
}
3、goroutine调度模型:
M代表操作系统里面的线程,P代表上下文,任何goroutine执行时,有上下文才能执行,保证执行状态、寄存器等,G代表goroutine(协程)
上图代表调度过程,两个M代表两个线程在跑,每个M挂四个协程,当前执行一个G,有三个在队列里面等待执行。从调度过程可以看出,一个物理线程可以跑多个goroutine,当创建上万个线程,实际在跑的goroutine可能只有几十个。因此,在go里不用担心创建的协程数太多导致系统响应不过来。
M0处,P执行G0的goroutine,另外三个goroutine处于等待状态,当G0要读文件执行io操作或读网络数据时,线程会阻塞,因为读文件,网络数据会很慢。P就执行不了任务,go调度机制会把M0-G0独立出来如右上角,并新创建一个线程M1,执行其他goroutine。
4、不同goroutine之间通讯方式
1)全局变量和锁同步
package main
import (
"fmt"
"sync"
"time"
)
var (
m = make(map[int]uint64)
lock sync.Mutex
)
type task struct{
n int
}
func calc(t *task){
var sum uint64
sum = 1
for i := 1;i<t.n;i++ {
sum *= uint64(i)
}
lock.Lock()
m[t.n] = sum
lock.Unlock()
}
func main(){
for i := 0; i<10;i++{
t := &task{n:i}
go calc(t)
}
time.Sleep(10*time.Second)
lock.Lock()
for k, v:=range m{
fmt.Printf("%d != %v\n", k, v)
}
lock.Unlock()
}
2)channel 管道
概念:goroutine之间经常需要进行通信,管道channel就是goroutine之间的通信连接,它可以让一个goroutine发送特定值到另一个goroutine的通信机制。每一个管道都有具体类型,这叫做管道的元素类型,它规定了通过这个管道能发送和接收的数据类型。
管道的关闭可以通过调用close函数来完成,对关闭后的管道执行发送操作将会触发panic。在一个已经关闭的管道上进行接收操作,将获取所有已经发送的值,直到通道为空。
channel特点:a、类似unix中管道(pipe)
b、先进先出
c、线程安全,多个goroutine同时访问,不需要加锁
d、channel是有类型的,一个整数的channel只能存放整数
channel声明:a、var 变量名 chan 类型
b、var test chan int
c、var test chan string
d、var test chan map[string]string
e、var test chan stu
f、var test chan *stu
实例代码:
package main
import "fmt"
type student struct{
name string
}
func main(){
var intChan chan int
intChan = make(chan int, 10)
intChan <- 10 //写入数据
var a int
a = <- intChan //读出数据
fmt.Println("a...", a)
//结构体
var stuChan chan student
stuChan = make(chan student, 10)
stu := student{name:"stu01"}
stuChan <- stu //写入数据
var stu02 student
stu02 = <- stuChan //读出数据
fmt.Println("stu02...", stu02)
//指针
var stuPChan chan *student
stuPChan = make(chan *student, 10)
stuPChan <- &stu //写入数据
var stu03 *student
stu03 = <- stuPChan
fmt.Println("stu03...", stu03)
}
channel和goroutine结合实例:
目的:写两个goroutine 一个往里面写数据,一个读出数据,并将结果打印出来。
package main
import (
"fmt"
"time"
)
func write(ch chan int){
for i := 0; i < 100; i++ {
ch <- i
}
}
func read(ch chan int){
for {
var b int
b = <- ch
fmt.Println(b)
}
}
func main(){
intChan := make(chan int, 10)
go write(intChan)
go read(intChan)
time.Sleep(10*time.Second)
}
判读管道是否关闭:
func main(){
var ch chan int
ch = make(chan int, 20)
for i := 0; i<20; i++{
ch <- i
}
close(ch)
for{
var b int
b, ok := <- ch
if ok == false{
fmt.Println("chan is close")
break
}
fmt.Println(b)
}
}
5、goroutine之间的通信实例
实例一:通过一个exitChan判断协程退出时关闭(1改进)
package main
import (
"fmt"
)
func write(intChan chan int, exitChan chan bool) {
for i := 0; i < 100; i++ {
intChan <- i
}
exitChan <- true
close(intChan)
}
func read(intChan chan int, exitChan chan bool) {
for {
i, ok := <-intChan
if !ok {
break
}
fmt.Println(i)
}
exitChan <- true
}
func main() {
intChan := make(chan int, 100)
exitChan := make(chan bool, 2)
go write(intChan, exitChan)
go read(intChan, exitChan)
for i := 0; i < 2; i++ {
<-exitChan
}
}
实例二:打印10万以内的所有素数(此处用到exitChan判断goroutine什么时候退出)
package main
import (
"fmt"
)
func calc(intChan chan int, resultChan chan int, exitChan chan bool) {
for v := range intChan {
flag := true
for i := 2; i < v; i++ {
if v%i == 0 {
flag = false
break
}
}
if flag {
resultChan <- v
}
}
exitChan <- true
}
func main() {
intChan := make(chan int, 1000)
resultChan := make(chan int, 1000)
exitChan := make(chan bool, 8)
go func() {
for i := 0; i < 100000; i ++ {
intChan <- i
}
close(intChan)
}()
for i := 0; i < 8; i++ {
go calc(intChan, resultChan, exitChan)
}
go func(){
for i := 0; i < 8; i++ {
<-exitChan
}
close(resultChan)
}()
for v := range resultChan {
fmt.Println(v)
}
}
6、select多路复用
select语句的形式其实和switch语句有点类似,这里每个case代表一个通信操作——在某个channel上发送或者接收,并且会包含一些语句组成的一个语句块 。
如果有多个管道操作准备完毕,select会随机选择其中一个执行。select中的default来设置当其它的操作都不能够马上被处理时程序需要执行的逻辑。
channel 的零值是nil, 并且对nil的channel 发送或者接收操作都会永远阻塞,在select语句中操作nil的channel永远都不会被select到。这可以让我们用nil来激活或者禁用case,来达成处理其他输出或者输出时间超时和取消的逻辑。
package main
import "fmt"
func main() {
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
for {
b := <-intChan
fmt.Println(b)
}
}
以上程序会阻塞,报死锁,用select解决,如下:
package main
import (
"fmt"
"time"
)
func main() {
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
for {
select {
case v := <-intChan:
fmt.Println(v)
default:
fmt.Println("time out")
time.Sleep(time.Second)
}
}
}
运行结果如下:
当两个管道时,哪个管道有数据就取哪个,当没数据时,运行default分支:
package main
import (
"fmt"
"time"
)
func main() {
intChan := make(chan int, 10)
intChan2 := make(chan int, 10)
go func() {
i := 0
for {
intChan <- i
time.Sleep(time.Second)
intChan2 <- i * i
i ++
}
}()
for {
select {
case v := <-intChan:
fmt.Println(v)
case b := <-intChan2:
fmt.Println(b)
default:
fmt.Println("time out")
time.Sleep(time.Second)
}
}
}
结果如下: