Go - 39 Go 协程+管道
进程和线程的说明:
1.进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。
2.线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
3.一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行。
4.一个程序至少有一个进程,一个进程至少有一个线程。
并发和并行:
1.多线程程序在单核上运行,就是并发。
并发:
因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
2.多线程程序在多核上运行,就是并行。
并行:
因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行。
Go协程和Go主线程:
1.Go主线程(也有直接称为线程/进程):一个Go线程上,可以起上万个协程,协程是轻量级的线程(底层是编译器做了优化)。
2.Go协程的特点:
有独立的栈空间
共享程序堆空间
调度由用户控制
协程是轻量级的线程
快速案例:
package main
import ( "fmt" "strconv" "time" )
func g_test() {
for i:=0; i < 100; i ++ {
fmt.Println("g_test() hello,world " + strconv.Itoa(i))
// 隔一秒
time.Sleep(time.Second)
}
}
func main() {
go g_test() // 开启一个协程
for i:=0; i < 10; i ++ {
fmt.Println("main() hello,golang " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
案例小结:
1.主线程是一个物理线程,直接作用在cpu上,是重量级的,非常耗费cpu资源。
2.协程从主线程开启的,是轻量级的线程,是逻辑态,对资源耗费相对小。
3.Golang的协程机制是重要的特点,可以轻松的开启上万个协程,其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了。
goroutine的调度模型:
MPG模式基本介绍:
M:操作系统的主线程(是物理线程)
P:协程执行需要的上下文
G:协程
Golang设置运行的cpu个数:
代码实现:
package main
import ( "fmt" "runtime" )
func main() {
cpuNum := runtime.NumCPU()
fmt.Println("win cpuNum=", cpuNum)
// 可以自己设置使用几个cpu
res := runtime.GOMAXPROCS(cpuNum-2)
fmt.Println("ok", res)
}
go1.8后,默认让程序运行在多个核上,可以不用设置了。
go1.8前,还是要设置一下的,可以更高效的利用cpu
全局变量互斥锁解决资源竞争:
协程并发(并行)出现资源竞争问题:
运行时增加 -race 参数,确实会发现有资源竞争问题(先编译:go build -race main.go 然后执行main.exe文件,会出现资源竞争问题)
sync包里的Mutex 里面的 Lock 和Unlock 方法来解决
实际案例:
package main
import ( "fmt" "time" "sync" )
// 全局互斥锁解决 资源竞争问题
var (
testMap = make(map[int]int)
// 声明一个全局互斥锁
// lock 是一个全局的互斥锁
// sync 是包 :synchornized 同步
// Mutex:互斥
lock sync.Mutex
)
func jc_test(n int) {
res := 1
for i := 1; i <= n; i ++ {
res *= i
}
fmt.Println("n! ===", n , res)
// 加锁
lock.Lock()
testMap[n] = res
// 解锁
lock.Unlock()
}
func main() {
// 使用循环来产生多个协程
for i:=1; i <= 200; i++ {
// 开启200个协程
go jc_test(i)
}
// 为了要执行协程内容 ,需要主线程中等待,不然会直接退出程序
time.Sleep(time.Second * 10)
// 打印出每个元素
lock.Lock()
for i, v := range testMap {
fmt.Printf("testMap[%d]==%d \n", i, v)
}
lock.Unlock()
// 按理说主线程等10秒 上面写的协程都应该执行完毕,后面读数据不应该出现资源竞争问题了
// 但实际运行中还是会出现资源竞争的
// 在读的地方加互斥锁,是因为我们程序设计上可以知道10秒执行完所有协程,但是主线程并不知道
// 因此底层可能任然出现资源争夺,因此加入互斥锁即可解决问题
}
上面的代码有个很大的问题:即我们主线程具体要等多久才合适呢?即引出了管道来实现协程间的通信;
channel(管道)基本介绍:
上面使用全局变量加锁同步来解决goroutine的通讯,但不是完美的解决方案;通讯机制--channel
channel的介绍:
1.channel本质就是一个数据结构--队列;
2.数据是先进先出;[FIFO]
3.线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的;
4.channel是有类型的,一个string的channel只能存放string类型数据;
定义/声明channel:
var 变量名 chan 数据类型
var intChan chan int --> intChan用于存放int数据
var mapChan chan map[int]string -->mapChan用于存放map[int]string类型
var perChan chan Person
var perChan chan *Person
说明:
1.channel是引用类型;
2.channel必须初始化才能写入数据,即make后才能使用;
3.管道是有类型的,intChan 只能写入整数int;
channel的基本使用:
// 定义一个管道
var intChan chan int
intChan = make(chan int, 3)
// 看管道是什么?
fmt.Println("intChan--", intChan) // intChan-- 0xc000104080
// 向管道写入数据
intChan<- 10
num := 20
intChan<- num
// 注意点:写入数据不能超过容量会报deadlock, 因为管道是用来一边写入一边取出的
intChan<- 30
// intChan<- 40
// 看管道的长度和容量(容量是定义时固定的不会动态增加)
fmt.Printf("intChan len=%v cap=%v \n", len(intChan), cap(intChan)) // intChan len=2 cap=3
// 从管道取出数据
var num2 int
num2 = <-intChan
fmt.Println("num2==",num2)
fmt.Printf("get intChan len=%v cap=%v \n", len(intChan), cap(intChan))
// 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取也会报deadlock
num3 := <-intChan
num4 := <-intChan
// num5 := <-intChan
fmt.Printf("num3==%d num4==%d \n", num3, num4)
// 当我们想放入任何类型的数据到管道内我们可以使用
var allChan chan interface{}
// 但是我们获取时,取出的每个元素就是空接口,要使用类型断言来解决 即:
cat := <-allChan
yuan_cat := cat.(Cat) // 此时的 yuan_cat 才是 Cat结构体,才能取出其中的属性值:yuan_cat.Name
channel的关闭和遍历
实际案例:
// channel 的关闭
intChan := make(chan int, 10)
intChan <- 10
intChan <- 20
close(intChan) // close 内置函数是用来专门关闭管道的
// intChan <- 30
// 关闭管道就不可以再向管道放数据了
fmt.Println("ok!!!")
// 但是可以获取数据
num := <- intChan
num2 := <- intChan
fmt.Println("num--", num, "num2--", num2)
// channel 的遍历 支持 for-range
// 在遍历时,如果channel没有关闭,则会出现deadlock的错误
// 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
intChan1 := make(chan int , 100)
// 写入数据
for i := 0; i < cap(intChan1); i++ {
intChan1<- i * 2
}
close(intChan1)
// 取数据
for v := range intChan1 {
fmt.Println("v=", v)
}
管道阻塞的机制:
如果只是向管道写入数据,而没有读取,就会出现阻塞而deadlock,原因是intChan容量是10,而writeData会写入50个数据,因此会阻塞在writeData的 intChan<- i
如果,编译器(运行)发现一个管道只有写,而没有读,则该管道,会阻塞。
写管道和读管道的频率不一致,无所谓。
综合案例:
利用goroutine和channel 实现1-200000数字中,哪些是素数(prime)
代码实现:
package main
import ( "fmt" )
func putNum(intChan chan int) {
for i:=1 ; i<= 200000; i ++ {
intChan<- i
}
close(intChan)
}
func checkPrime(intChan chan int, primeChan chan int, exitChan chan bool) {
for {
num, ok := <-intChan
if !ok {
break
}
flag := true // 素数标识
for i := 2; i < num; i ++ {
if num % i == 0 {
flag = false
break
}
}
if flag {
primeChan<- num
}
}
fmt.Println("有一个primeCheck 管道完成任务, 退出")
// 这里不能关闭 primeChan 因为可能别的协程还在写数据
// 要写标识退出的 true到 exitChan里
exitChan<- true
}
func main() {
// 协程求素数 1-200000数字中,哪些是素数?
// 传统for循环方法实现没有问题,但是无法有效利用多核
// 将统计素数的任务分配给多个(6)个协程
intChan := make(chan int, 10000)
primeChan := make(chan int, 100000)
exitChan := make(chan bool, 6)
go putNum(intChan)
// 开启6个协程 统计素数并且放入 primeChan中
for i := 0; i < 6; i ++ {
go checkPrime(intChan, primeChan, exitChan)
}
// 主线程处理
go func(){
for i := 0; i < 6; i ++ {
// 不断地从 exitChan 中取值,只有这样主线程才不会马上退出
<-exitChan
}
// 取完说明 所有协程已经把数据全部放入到 primeChan中,然后关闭管道
close(primeChan)
}()
// 遍历 primeChan 取出结果
for {
v, ok := <-primeChan
if !ok {
break
}
fmt.Println("prime num ==", v)
}
fmt.Println("main 主线程退出~~~")
}
传统方式:
package main
import ( "fmt" "time" )
func main() {
start := time.Now().Unix()
for num := 1; num <= 200000; num ++ {
flag := true // 素数标识
for i := 2; i < num; i ++ {
if num % i == 0 {
flag = false
break
}
}
if flag {
}
}
end := time.Now().Unix()
fmt.Println("传统方式耗时--", end - start)
}
效率相比,goroutine + channel 比传统方法 快基本是cpu的个数的倍数;
也可以打开任务管理器看一下开启多个协程时,cpu的使用率(go协程的话开和cpu相同的协程数的话跑的过程中,cpu使用率基本可以达到100%,而传统方式只会提高使用率但是不会达到100%)