文章目录
- goroutine
- 概念
- goroutine调度模型
- channel
- channel介绍
- 定义/声明channel
- channel的关闭
- channel遍历
- channel其他细节
goroutine
前言:统计1~90000000数字中,哪些是素数?
- 使用循环,很慢
- 使用并发或者并行的方式,将任务分配给多个goroutine去完成,就会使用到goroutine
概念
进程和线程
进程
是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位线程
是进程的一个执行实例,是程序执行的最小单元,是比进程更小的能独立运行的基本单位- 一个进程可以创建核销多个线程,同一个进程中的多个线程可以并发执行
- 一个程序至少有一个进程,一个进程至少有一个线程
并发和并行
- 多线程程序在单核上运行,就是并发
- 多线程程序在多核上运行,就是并行
GO协程和GO主线程
- go主线程(直接称为线程/也可理解为进程):一个go线程上,可以起多个协程,可以理解为协程是轻量级的线程[编译器做优化]
- go协程的特点:
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
package main
import (
"fmt"
"strconv"
"time"
)
func test(){
for i:=1;i<=5;i++{
fmt.Println("test() hello,world! "+strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() //开启一个协程
for i:=1;i<=5;i++{
fmt.Println("main() hello,golang! "+strconv.Itoa(i))
time.Sleep(time.Second)
}
}
output:
- 主线程是一个物理线程,直接作用在cpu,是重量级的,非常耗费cpu资源
- 协程从主线程开启,是轻量级的线程,是逻辑态,对资源消耗相对较小
- golang的协程机制可以轻松开启上万个协程,其他编程语言的并发机制一般基于线程,开启过多线程,资源消耗大,这就凸显Golang在并发上的优势
goroutine调度模型
MPG模式
MPG模式运行状态1
MPG模式运行状态2
channel
前言:计算1~200各个数的阶乘,并且把各个数的阶乘放到map中。显示出来。用goroutine完成
- 用goroutine完成,效率高,会出现并发/并行安全问题
- 不同goroutine如何通信?
尝试的解决方案:
package main
import (
"fmt"
"time"
)
var (
myMap=make(map[int]int,10)
)
//计算阶乘
func test(n int){
res:=1
for i:=1;i<=n;i++{
res*=i
}
myMap[n]=res
}
func main() {
for i:=1;i<=200;i++{
go test(i)
}
time.Sleep(time.Second*10)
for i,v:=range myMap{
fmt.Printf("map[%d]=%d\n",i,v)
}
}
运行结果:
不同goroutine如何通信
1、全局变量的互斥锁
2、使用管道channel来解决
使用全局变量加锁同步改进程序
未对全局变量m加锁,会出现资源争夺问题,代码报错,提示concurrent map writes
解决方案:加入互斥锁
tips:阶乘很大,结果可能越界,可改为sum+=unint64(i)
改进后代码:
package main
import (
"fmt"
"sync"
"time"
)
var (
myMap=make(map[int]int,10)
// 声明一个全局互斥锁
//lock是一个全局变量互斥锁
//sync是包:synchornized同步
//Mutex:互斥
lock sync.Mutex
)
//计算阶乘
func test(n int){
res:=1
for i:=1;i<=n;i++{
res*=i
}
lock.Lock()
myMap[n]=res
lock.Unlock()
}
func main() {
for i:=1;i<=200;i++{
go test(i)
}
time.Sleep(time.Second*10)
lock.Lock()
for i,v:=range myMap{
fmt.Printf("map[%d]=%d\n",i,v)
}
lock.Unlock()
}
运行结果:
为什么需要channel?
- 使用全局变量加锁同步来解决goroutine通讯,不利于多个协程对全局变量的读写操作
- 主线程等待所有goroutine全部完成的时间很难确定
- 主线程休眠时间长,会加长等待时间,等待时间短,goroutine可能处于工作状态,会随着主线程的退出而消亡
- 引出新的通讯机制-channel
channel介绍
- 本质是一个数据结构-队列
- 数据先进先出FIFO
- 线程安全,多goroutine访问,不需要加锁
- 多类型,一个string的channel只能存放string类型
定义/声明channel
- var 变量名 chan 数据类型
- var intChan chan int
- var mapChan chan map[int]string
- 注:channel是引用类型,必须初始化才能写入数据,即make之后才能使用
package main
import "fmt"
func main(){
var intChan chan int
intChan=make(chan int,3)
fmt.Printf("intchan 的值=%v intchan地址=%p\n",intChan,&intChan)
// 写入数据
intChan<- 10
num:=211
intChan<- num
intChan<- 50
// 不要超过容量
fmt.Printf("channel len=%v cap=%v\n",len(intChan),cap(intChan))
// 读数据
var num2 int
num2=<- intChan
fmt.Println("num2=",num2)
fmt.Printf("channel len=%v cap=%v\n",len(intChan),cap(intChan))
// 没有使用协程的情况下,如果channel数据已经全部取出,再取就会报错
num3:=<- intChan
num4:=<- intChan
num5:=<- intChan
fmt.Println(",num3=",num3,",num4=",num4,",num5=",num5)
}
令num5=0之后:
- channel数据放满后,不能再放入
- channel数据取出后,可以再放数据
- 没有使用协程的情况下,channel数据取完了,再取,就会报dead lock
examples:
package main
import "fmt"
type Cat struct {
Name string
Age int
}
func main(){
var mapChan chan map[string]string
mapChan=make(chan map[string]string,10)
m1:=make(map[string]string,20)
m1["city1"]="北京"
m1["city2"]="天津"
m2:=make(map[string]string,20)
m2["hero1"]="ldh"
m2["hero2"]="glt"
mapChan<- m1
mapChan<- m2
var catChan chan Cat
catChan=make(chan Cat,10)
cat1:=Cat{"tom",10}
cat2:=Cat{"jarry",5}
catChan<- cat1
catChan<- cat2
cat11:=<- catChan
cat22:=<- catChan
fmt.Println("cat11,cat22:",cat11,cat22)
var catChan2 chan *Cat
catChan2=make(chan *Cat,10)
catChan2<- &cat1
catChan2<- &cat2
cat111:=<- catChan2
cat222:=<- catChan2
fmt.Println("cat111,cat222:",cat111,cat222)
// 任意类型数据
var allChan chan interface{}
allChan=make(chan interface{},10)
allChan<- cat1
allChan<- cat2
allChan<- 10
allChan<- m1
catt:=<- allChan
fmt.Println("catt=",catt)
//fmt.Println("catt.name:",catt.Name)//error: catt.Name undefined (type interface{} has no field or method Name)
//使用类型断言
cattt:=catt.(Cat)
fmt.Println("cattt.name:",cattt.Name)
}
channel的关闭
- 使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据,但还可以从该channel读数据
package main
import "fmt"
func main() {
intChan :=make(chan int,10)
intChan<- 1
intChan<- 2
intChan<- 3
close(intChan)
//intChan<- 4//报错:panic: send on closed channel
n1:=<- intChan
fmt.Println("n1:",n1)
//输出:n1: 1
}
channel遍历
- 支持for-range遍历
- 遍历时,如果channel没有关闭,出现deadlock错误
- 遍历时,如果channel已经关闭,会正常遍历数据
package main
import "fmt"
func main() {
intChan :=make(chan int,20)
for i:=0;i<20;i++{
intChan<- i
}
close(intChan)
for v:=range intChan{
fmt.Println("v=",v)
}
}
//输出:
//v= 0
//v= 1
//v= 2
//v= 3
//v= 4
//v= 5
//v= 6
//v= 7
//v= 8
//v= 9
//v= 10
//v= 11
//v= 12
//v= 13
//v= 14
//v= 15
//v= 16
//v= 17
//v= 18
//v= 19
案例:
package main
import "fmt"
func writeData(intChan chan int){
for i:=1;i<=50;i++{
intChan<- i
fmt.Println("writeData:",i)
}
close(intChan)
}
func readData(intChan chan int,exitChan chan bool) {
for{
v,ok:=<- intChan
if !ok{
break
}
fmt.Println("readDate:",v)
}
exitChan<- true
close(exitChan)
}
func main() {
intChan :=make(chan int,50)
exitChan:=make(chan bool,1)
go writeData(intChan)
go readData(intChan,exitChan)
for{
_,ok:=<- exitChan
if !ok{
break
}
}
}
writeData: 1
writeData: 2
writeData: 3
writeData: 4
writeData: 5
writeData: 6
writeData: 7
writeData: 8
writeData: 9
writeData: 10
writeData: 11
writeData: 12
writeData: 13
writeData: 14
writeData: 15
writeData: 16
writeData: 17
writeData: 18
writeData: 19
writeData: 20
writeData: 21
writeData: 22
writeData: 23
writeData: 24
writeData: 25
writeData: 26
writeData: 27
writeData: 28
writeData: 29
writeData: 30
writeData: 31
writeData: 32
writeData: 33
writeData: 34
writeData: 35
writeData: 36
writeData: 37
writeData: 38
writeData: 39
writeData: 40
writeData: 41
writeData: 42
writeData: 43
writeData: 44
writeData: 45
writeData: 46
writeData: 47
writeData: 48
writeData: 49
writeData: 50
readDate: 1
readDate: 2
readDate: 3
readDate: 4
readDate: 5
readDate: 6
readDate: 7
readDate: 8
readDate: 9
readDate: 10
readDate: 11
readDate: 12
readDate: 13
readDate: 14
readDate: 15
readDate: 16
readDate: 17
readDate: 18
readDate: 19
readDate: 20
readDate: 21
readDate: 22
readDate: 23
readDate: 24
readDate: 25
readDate: 26
readDate: 27
readDate: 28
readDate: 29
readDate: 30
readDate: 31
readDate: 32
readDate: 33
readDate: 34
readDate: 35
readDate: 36
readDate: 37
readDate: 38
readDate: 39
readDate: 40
readDate: 41
readDate: 42
readDate: 43
readDate: 44
readDate: 45
readDate: 46
readDate: 47
readDate: 48
readDate: 49
readDate: 50
Process finished with exit code 0
案例:开头的例子
统计1~8000的数字中哪些是素数
package main
import (
"fmt"
"time"
)
func putNum(intChan chan int){
for i:=1;i<=8000;i++{
intChan<- i
}
close(intChan)
}
//从intChan取出数据,并判断是否为素数,是,就放入primeChan
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool){
var flag bool
for{
time.Sleep(time.Millisecond*10)
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("有一个primeNum协程因为取不到数据,退出")
exitChan<- true
}
func main(){
intChan:=make(chan int,1000)
primeChan:=make(chan int,2000)
exitChan:=make(chan bool,4)
//开启一个协程,向intChan放入1~8000个数
go putNum(intChan)
//开启4个协程,从intChan取出数据,并判断是否为素数,如果是,就放到primeChan
for i:=0;i<4;i++{
go primeNum(intChan,primeChan,exitChan)
}
//这里是主线程,直接进行处理
go func() {
for i:=0;i<4;i++{
<- exitChan
}
// 当从exitChan中取出4个结果,就可以放心关闭primeChan
close(primeChan)
}()
//遍历primeChan,取出结果
for{
res,ok:=<- primeChan
if !ok{
break
}
fmt.Printf("素数=%d\n",res)
}
fmt.Println("main线程退出")
}
channel其他细节
- channel可以声明只读或只写
- select可解决从管道取数据阻塞的问题
//默认,管道是双向
var chan1 chan int
//只写
var chan2 chan<- int
//只读
var chan1 <-chan int