1.goroutine
1.1 进程和线程介绍1.2 并行和并发1.3 Go协程和Go主线程1.4 goroutine快速入门
(1)案例说明(2)代码实现(3)主线程和协程执行流程图(4)快速入门小结 1.5 goroutine的调度模型
(1)MPG模式基本介绍(2)MPG模式运行的状态1(3)MPG模式运行的状态2 1.6 设置Golang运行的cpu数 2.channel
2.1 需求2.2初步实现,但有问题2.3 不同goroutine之间如何通讯2.4 使用全局变量加锁同步改进程序2.5 为什么需要channel2.6 Channel的基本介绍2.7 定义/声明channel2.8 管道初始化2.9 读写channel案例2.10 channel的关闭2.11channel的遍历2.12 应用实例2.13阻塞2.14 应用goroutine和channel2.15 channel只读或只写2.16 使用select可以解决从管道取数据的阻塞问题2.17 goroutine中使用recover解决出现panic问题
1.goroutine 1.1 进程和线程介绍● 进程是程序在 *** 作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
● 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
● 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
● 一个程序至少有一个进程,一个进程至少有一个线程
● 多线程程序在单核上运行,就是并发
● 多线程程序在多核上运行,就是并行
● 并发:因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询 *** 作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
● 并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行
● Go主线程(有程序员直接称为线程/也可理解为进程):一个Go线程上,可以起多个协程,可以这样理解,协程是轻量级的线程【编译器做优化】 ● Go协程的特点 ○ 有独立的栈空间 ○ 共享程序堆空间 ○ 调度由用户控制 ○ 协程是轻量级的线程1.4 goroutine快速入门 (1)案例说明
编写一个程序,完成如下功能:
在主线程(可以理解为进程)中,开启一个goroutine,该协程每隔1秒输出“hello,world”
在主线程也每隔一秒输出“hello,golang”,输出10次后,退出程序
要求主线程和goroutine同时执行
package main import ( "fmt" "strconv" "time" ) func test() { for i := 1; i <= 10; i++ { fmt.Println("test() hello,world" + strconv.Itoa(i)) time.Sleep(time.Second) } } func main() { go test() //开启一个协程 for i := 1; i <= 10; i++ { fmt.Println("main() hello,golang" + strconv.Itoa(i)) time.Sleep(time.Second) } }(3)主线程和协程执行流程图 (4)快速入门小结
● 主线程是以一个物理线程,直接作用在cpu上。是重量级的,非常消耗cpu资源
● 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对较小
● golang的协程机制是个重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制一般是基于线程的,开启过多的线程,资源耗费大,这里就凸显出golang在并发上的优势了
M: *** 作系统的主线程(是物理线程)
P:协程执行需要的上下文
G:协程
● 当前程序有三个M,如果三个M都在一个cpu运行,就是并发,如果在不同的cpu运行就是并行
● M1,M2,M3正在执行一个G,M1的协程队列有三个,M2的协程队列有3个,M3协程队列有2个
● 从上图可以看到: Go的协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程。
● 其它程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu
● 分成两个部分来看
● 原来的情况是MO主线程正在执行G0协程,另外有三个协程在队列等待
● 如果GO协程阻塞,比如读取文件或者数据库等,这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行, MO的主线程下的GO仍然执行文件io的读写。
● 这样的MPG调度模式,可以既让GO执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行。
● 等到G0不阻塞了,MO会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒。
package main import ( "fmt" "runtime" ) func main() { //获取当前cpu数 返回的是本地机器的逻辑cpu数 num := runtime.NumCPU() //设置运行go程序cpu数 runtime.GOMAXPROCS(num - 1) fmt.Println("num=", num) }
● go1.8后,默认让程序运行在多个核上,可以不用设置了
● go1.8前,需要设置
需求:计算1-200的各个数的阶乘,并把各个数的阶乘放入到map中,最后显示出来,需要使用goroutine完成
2.2初步实现,但有问题package main import "fmt" var myMap = make(map[int]int, 10) //test函数用来计算阶乘并将结果放入到map中 func test(n int) { res := 1 for i := 1; i <= n; i++ { res *= i } myMap[n] = res } func main() { //开启多个协程完成这个任务【200个】 for i := 1; i <= 200; i++ { go test(i) } //休眠10秒钟 time.Sleep(time.Second * 10) //输出结果 for i, v := range myMap { fmt.Printf("map[%d]=%dn", i, v) } }
直接运行报错,因为对map并发的写
如何判断程序是否存在资源竞争问题,在编译程序时,增加一个参数 -race 即可
go build -race main.go
如果存在资源竞争问题,会打印出来
如果不休眠,程序运行不完整
● 全局变量的互斥锁
● 使用管道channel来解决
● 因为对全局变量myMap没有加锁,因此会出现资源争夺问题,代码会出现错误,提示fatal error: concurrent map writes错误
● 解决方法:加入互斥锁
package main import ( "fmt" "sync" ) var ( myMap = make(map[int]int, 10) //声明一个全局的互斥锁 //lock 是一个全局的互斥锁 //sync 是包 //Mutex 是互斥 lock sync.Mutex ) //test函数用来计算阶乘并将结果放入到map中 func test(n int) { res := 1 for i := 1; i <= n; i++ { res *= i } //加锁 lock.Lock() myMap[n] = res //解锁 lock.Unlock() } func main() { //开启多个协程完成这个任务【200个】 for i := 1; i <= 200; i++ { go test(i) } //休眠10秒钟 time.Sleep(time.Second * 10) //输出结果 //加锁 lock.Lock() for i, v := range myMap { fmt.Printf("map[%d]=%dn", i, v) } //解锁 lock.Unlock() }
没有报错
为什么输出结果也要加锁呢?
因为主线程不知道协程什么时候执行完,因此底层可能仍然出现资源争夺
map[n]的结果为0,是因为结果超出int范围了
● 前面使用全局变量加锁同步来解决goroutine的通讯,但不完美
● 主线程在等待所有goroutine全部加载完成的时间很难确定,我们这里设置10秒,仅仅是估算
● 如果主线程休眠时间长了,会加长等待时间,如果的等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁
● 通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写 *** 作
● channel本质就是一个数据结构-队列
● 数据是先进先出
● 线程安全,多个goroutine访问时,不需要加锁,channel本身是线程安全的
● channel有类型,一个string的channel只能存放string类型的数据
var 变量名 chan 数据结构 举例: var intChan chan int var mapChan chan map[int]string var perChan chan Person var perChan2 chan *Person 说明: channel是引用类型 channel必须初始化才能写入数据,即make后才能使用 管道是有类型的,intChan只能写入整数int2.8 管道初始化
package main import "fmt" func main() { //1.创建一个可以存放三个int类型的管道 var intChan chan int intChan = make(chan int, 3) //2.intChan是什么 fmt.Printf("intChan的值是=%vn", intChan) //intChan的值是=0xc0000d4080 //3.向管道写入数据 intChan <- 19 intChan <- 12 intChan <- 23 //intChan<-50//报错,当我们给管道写入数据时不能超过其容量 //4.管道长度和容量 fmt.Printf("channel len=%v cap=%vn", len(intChan), cap(intChan)) //channel len=3 cap=3 //5.从管道中读取数据 var num int num = <-intChan fmt.Println("num=", num) //num= 19 fmt.Printf("channel len=%v cap=%vn", len(intChan), cap(intChan)) //channel len=2 cap=3 //6.在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取出就会报错deadlock num2 := <-intChan num3 := <-intChan //num4 := <-intChan//报错 fmt.Println(num2, num3) //12 23 }
注意事项:
● channel中只能存放指定的数据类型
● channel数据放满后,就不能再放了
● 如果从channel取出数据后,可以继续放
● 在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
注意:
package main import "fmt" type Cat struct { Name string Age int } func main() { var allChan chan interface{} allChan = make(chan interface{}, 10) cat1 := Cat{Name: "tom", Age: 10} cat2 := Cat{Name: "tom~~", Age: 20} allChan <- cat1 allChan <- cat2 allChan <- 121 allChan <- "abc" //读取 cat111 := <-allChan cat222 := <-allChan v1 := <-allChan v2 := <-allChan fmt.Println(cat111, cat222, v1, v2) //{tom 10} {tom~~ 20} 121 abc 这样输出没有问题 //fmt.Println("Cat Name:",cat111.Name)//编译就报错cat111.Name undefined (type interface{} has no field or method //如果要拿到name,必须使用断言 a := cat111.(Cat) fmt.Println("Cat Name:", a.Name) //Cat Name: tom }2.10 channel的关闭
使用内置函数close就可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据
package main import "fmt" func main() { intChan := make(chan int, 3) intChan <- 23 intChan <- 34 close(intChan) //关闭后如果写入会报错 //intChan <- 45 //panic: send on closed channel // 关闭后可以正常读取数据 n1 := <-intChan fmt.Println(n1) //23 }2.11channel的遍历
channel支持for-range的方式进行遍历。需要注意两点
● 在遍历时,如果channel没有关闭,则会出现deadlock的错误
● 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完成后,就会退出遍历
package main import "fmt" func main() { intChan1 := make(chan int, 100) for i := 1; i <= 100; i++ { intChan1 <- i * 2 } //遍历管道不能用普通的for循环,因为长度一直在变化 //在遍历时,如果channel没有关闭,则会出现deadlock的错误 // 打印完数据并报fatal error: all goroutines are asleep - deadlock! //在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完成后,就会退出遍历 close(intChan1) for v := range intChan1 { fmt.Println("v=", v) } }2.12 应用实例
使用goroutine和channel协同工作完成案例
要求:
● 开启一个writeData协程,向管道intChan中写入50个整数
● 开启一个readData协程,向管道intChan中读取writeData写入的数据
● 注意:writeData和readData *** 作的是同一个管道
● 主线程需要等待writeData和readData协程都完成后才能退出
思路分析:
package main import "fmt" //writeData func writeData(intChan chan int) { for i := 1; i <= 50; i++ { intChan <- i fmt.Println("writedata:", i) } close(intChan) } //readData func readData(intChan chan int, exitChan chan bool) { for { v, ok := <-intChan if !ok { break } fmt.Println("readData 读到的数据为:", v) } //数据读取完成后,即任务完成 exitChan <- true close(exitChan) } func main() { //创建2个管道 intChan := make(chan int, 10) exitChan := make(chan bool, 1) go writeData(intChan) go readData(intChan, exitChan) for { _, ok := <-exitChan if !ok { break } } }2.13阻塞
如果将上面代码的读数据注释go readData(intChan, exitChan),就会发生阻塞
运行结果:
writeData: 1 writeData: 2 writeData: 3 writeData: 4 writeData: 5 writeData: 6 writeData: 7 writeData: 8 writeData: 9 writeData: 10 fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main() C:/Project/GolangProject/src/go_code/Project01/chapter16/demo6/main.go:37 +0x92 goroutine 19 [chan send]: main.writeData(0x0) C:/Project/GolangProject/src/go_code/Project01/chapter16/demo6/main.go:8 +0x3b created by main.main C:/Project/GolangProject/src/go_code/Project01/chapter16/demo6/main.go:33 +0x86 exit status 2
如果程序运行发现一个管道只有写,没有读,则该管道会发生阻塞。写管道和读管道的频率不一致无所谓
2.14 应用goroutine和channel需求:统计1-8000中,哪些是素数?
思路:
● 传统:使用一个循环,循环判断
● 使用并发/并行:将统计素数的任务分配给多个goroutine去完成,完成任务时间短
package main import ( "fmt" ) //将1-8000写入管道 func putNum(intChan chan int) { for i := 1; i <= 8000; i++ { intChan <- i } close(intChan) } //求素数,如果是就放到primeChan管道 func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) { var flag bool for { num, ok := <-intChan if !ok { //取不到了 break } flag = true //判断num是不是素数 for i := 2; i < num; i++ { if num%i == 0 { //说明概述不是素数 flag = false break } } if flag { primeChan <- num } } fmt.Println("有一个primeNu协程因为取不到数据退出!!!") exitChan <- true } func main() { intChan := make(chan int, 1000) primeChan := make(chan int, 2000) exitChan := make(chan bool, 4) start := time.Now().Unix() //开启一个协程,向intChan中放入1-80000 go putNum(intChan) //开启四个协程,从intChan取出数据,并判断是否为素数,如果是就放到primeChan for i := 0; i < 4; i++ { go primeNum(intChan, primeChan, exitChan) } //主线程直接处理 go func() { for i := 0; i < 4; i++ { <-exitChan } end := time.Now().Unix() fmt.Println("使用协程耗时=", end - start) //当我们从exitChan取出4个结果,就可以放心的关闭primeChan close(primeChan) }() //遍历primeChan,取出结果 for { res, ok := <-primeChan if !ok { break } fmt.Println("素数=", res) } fmt.Println("main()线程退出") }
使用4个协程后,执行速度比普通方法提高至少4倍
2.15 channel只读或只写package main import "fmt" func main() { //管道可以声明为只读或只写 //1.在默认情况下,管道是双向 //var channel chan int //可读可写 //声明为只写 var chan2 chan<- int chan2 = make(chan int, 3) chan2 <- 20 //num:=<-chan2//报错invalid operation: cannot receive from send-only channel chan2 (variable of type chan<- int) fmt.Println("chan2=", chan2) //声明为只读 var chan3 <-chan int num2 := <-chan3 // chan3<-10//报错 fmt.Println("num2=", num2) }2.16 使用select可以解决从管道取数据的阻塞问题
package main import ( "fmt" "strconv" ) func main() { //1.定义一个管道10个数据int intChan := make(chan int, 10) for i := 0; i < 10; i++ { intChan <- i } //2.定义一个管道5个数据string stringChan := make(chan string, 5) for i := 0; i < 5; i++ { stringChan <- "hello" + strconv.Itoa(i) } //label //传统的方法在遍历管道时,如果不关闭会阻塞而导致死锁deadlock //在实际开发中,我们不好确定什么时候关闭管道,可以用select方式解决 for { select { //如果intChan一直没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配 case v := <-intChan: fmt.Println("从intChan读取的数据:", v) case v := <-stringChan: fmt.Println("从stringChan读取的数据:", v) default: fmt.Println("都取不到数据:") return //或使用标签 break label } } }2.17 goroutine中使用recover解决出现panic问题
如果一个协程出现了panic,如果没有捕获这个panic就会造成整个程序崩溃
这时我们需要在协程中使用revover来捕获panic进行处理,这样即使这个协程发生问题,主线程不会受影响,可以继续执行
package main import ( "fmt" "time" ) func sayHello() { for i := 0; i < 10; i++ { fmt.Println("hello,world", i) } } func test() { //定义一个map,没有make就赋值 var myMap map[int]string myMap[0] = "golang" //报错 } func main() { go sayHello() go test() for i := 0; i < 10; i++ { fmt.Println("main()ok", i) time.Sleep(time.Second) } }
不处理错误,程序报错并停止
解决
package main import ( "fmt" "time" ) func sayHello() { for i := 0; i < 10; i++ { fmt.Println("hello,world", i) } } func test() { defer func() { //捕获test抛出的panic if err := recover(); err != nil { fmt.Println("test()发生错误", err) } }() //定义一个map,没有make就赋值 var myMap map[int]string myMap[0] = "golang" //报错 } func main() { go sayHello() go test() for i := 0; i < 10; i++ { fmt.Println("main()ok", i) time.Sleep(time.Second) } }
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)