goroutine基本介绍
假如我们要计算 1-200000里哪些是素数?
1:我们传统方法就是使用一个循环,循环判断各个数是不是素数。
2:使用并发或者并行的方式,将统计素数的任务分配给多个goroutine去完成。
进程和线程说明
1:进程就是程序在操作系统中的一个执行过程,是系统进行资源分配和调度的基本单位。
2:线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
3:一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行。
4:一个程序至少有一个进程,一个进程至少有一个线程。
并发和并行
1:多线程程序在单核上运行,就是并发
2:多线程程序在多核上运行,就是并行
go协程和go主线程
1:go主线程(有程序员直接称为线程/也可以理解成进程):一个go线程上,可以起多个协程,你可以理解成:协程是轻量级线程。【编译器做优化】
2:go协程的特点
(1)有独立的栈空间
(2)共享程序堆空间
(3)调度由用户控制
(4)协程是轻量级线程
goroutine案例演示
编写一个程序,完成如下功能:
1:在主线程中,开启一个goroutine,该协程每隔1秒输出“hello”
2:在主线程中也每隔一秒输出“go”,输出10次后,退出程序
3:要求主线程和goroutine同时运行。
package main
import (
"fmt"
"time"
)
func test(){
for i:=1; i <= 10; i++{
fmt.Println("hello",i)
time.Sleep(time.Second)
}
}
func main() {
go test()
for i:=1; i <= 10; i++{
fmt.Println(" go",i)
time.Sleep(time.Second)
}
}
小结:
1:主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源。
2:协程从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小。
3:go语言的协程机制是重要的特点,可以轻松的开启上万个协程。其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显go语言的并发上的优势。
goroutine的调度模型
MPG模式基本介绍
M:操作系统的主线程 (是物理线程)
P:协程执行需要的上下文,可以看做一个局部调度器,使go代码在一个线程上跑,它是实现从N:1到N:M映射的关键。( 就是需要的资源和当时运行的状态)
G:协程,代表一个goroutine,它有自己的栈,instruction pointer和其它信息(正在等待的channel等等),用于调度
设置go语言运行的cpu数
package main
import (
"fmt"
"runtime"
)
func main() {
//获取当前电脑运行的cpu数
num := runtime.NumCPU()
fmt.Println("CPUnum=",num)
//可以设置主键使用多少个CPU,这里我保留一个
runtime.GOMAXPROCS(num - 1)
fmt.Println("ok")
}
go1.8后,默认让程序运行在多个核上,可以不用设置
go1.8前,需要设置一下,可以高效的利用cpu
channel(管道)
1:channel本质就是一个数据结构-队列
2:数据先进先出
3:线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
4:channel是有类型的,一个string的channel只能存放string类型数据。
定义/声明
var 变量名 chan 数据类型
举例:
var intChan chan int (用于存放int数据)
var mapChan chan map[int]string(用于存放map[int]string数据)
car perChan chan Person
。。。
说明:
1:channel是引用类型
2:channel必须初始化才能写入数据,即make后才能使用
3:管道是有类型的额,如intChan只能写入整数int类型
初始化:
intChan = make(chan int,3)
channel可以声明为只读,或者只写的性质
//声明只写
var chan2 chan<- int
chan2 = make(chan int,3)
chan2<- 33
//声明只读
var chan3 <-chan int
num := <-chan3
向管道写入数据:
intChan <- 10
从管道读取数据:
num:= <- intChan
channel使用细节说明:
1:channel中只能存放指定的数据类型
2:channel的数据放满后,就不能再放入了
3:如果从channel取出数据后,可以继续放入
4:在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
channel的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据。
channel的遍历
channel支持for-range的方式进行遍历。
注意:在遍历时,如果channel没有关闭,则会出现dead lock的报错
//在遍历时,要先关闭channel
clase(intChan)
//遍历管道是不能使用普通的for循环
//这里的for-range没有i这个输出下标,所以直接 如下就可以遍历
for v:= range intChan {
fmt.Println("v=",v)
}
goroutine和channel结合的应用案例
1:开启两个协程,一个协程向管道写入50个整数,一个协程从管道读取数据。
package main
import (
"fmt"
"time"
// "runtime"
)
var (
myMap = make(map[int]int,10)
)
func writeData(intChan chan int){
for i:=1; i<=50; i++{
intChan <- i
fmt.Printf("写入数据=%vn",i)
//加个时间休眠来看出这两个协程是同步的
time.Sleep(time.Second)
}
close(intChan)
}
func readData(intChan chan int,boolChan chan bool){
for{
v,ok := <- intChan
if !ok{
break
}
fmt.Printf("读出数据为=%vn",v)
time.Sleep(time.Second)
}
boolChan <- true
close(boolChan)
}
func main() {
var intChan chan int
intChan = make(chan int,50)
boolChan := make(chan bool,1)
go writeData(intChan)
go readData(intChan,boolChan)
for {
_,ok := <-boolChan
if !ok{
break
}
}
}
阻塞说明:
如果,在编译器在运行时,发现一个管道只有写,没有读,则该管道就会阻塞。
应用案例2: 要求统计1-6666的数字中,哪些数是素数,使用并发/并行的方式,将统计素数的任务分配给4个goroutine去完成。
package main
import (
"fmt"
"time"
// "runtime"
)
func writeData(intChan chan int){
for i:=1; i<=6666; i++{
intChan <- i
}
close(intChan)
}
//从intchan取出数据并判断是否为素数,如果是就放入primeChan
func primeNum(intChan chan int,primeChan chan int,boolChan chan bool){
var flag bool
for{
time.Sleep(time.Millisecond)
num,ok := <- intChan
if !ok{ //intchan里面娶不到东西了
break
}
flag = true
for i := 2; i< num; i++{
if num % i == 0{
flag = false
break
}
}
if flag{
primeChan<- num
}
}
boolChan <- true
}
func readData(primeChan chan int){
for{
v,ok := <- primeChan
if !ok{
break
}
fmt.Printf("素数为=%vn",v)
}
}
func main() {
var intChan chan int
intChan = make(chan int,1000)
primeChan := make(chan int,6666) //放入素数结果
boolChan := make(chan bool,4)
//开启一个协程,向intchan放入1-6666个数
go writeData(intChan)
//开启4个协程,从intChan取出数据并判断是否为素数
for i:=1; i <= 4; i++{
go primeNum(intChan,primeChan,boolChan)
}
go func(){
for {
if len(boolChan) == 4{
close(primeChan)
break
}
}
}()
readData(primeChan)
}
用select解决管道阻塞问题案例
代码:
package main
import (
"fmt"
_"time"
// "runtime"
)
func main() {
var intChan chan int
intChan = make(chan int,10)
for i := 1; i<=10;i++{
intChan<- i
}
stringChan := make(chan string,4)
for i := 1; i<=4; i++{
stringChan<- fmt.Sprintf("lalala %d",i)
}
//传统方法在遍历管道的时候,如果不关闭会阻塞
//所以在实际开发中,我们可能会遇到一种情况:我们不好确定什么时候关
//可以使用select方式解决
for{
select{
case v := <- intChan :
fmt.Printf("intChan数据有 %dn",v)
case v:= <- stringChan :
fmt.Printf("stringChan数据为 %sn",v)
default :
fmt.Println("nothing ,over ")
return
}
}
}
goroutine错误机制处理
我们在goroutine中使用recover,解决协程中出现panic,导致程序崩溃停止的问题
package main
import (
"fmt"
"time"
// "runtime"
)
func say(){
for i := 1; i<=10;i++{
time.Sleep(time.Second)
fmt.Println("go",i)
}
}
func test(){
//这里defer函数来处理错误
defer func(){
//使用recover来捕获panic进行处理,这样协程发生问题不会影响其他线程
if err := recover(); err != nil{
fmt.Println("test 错误=",err)
}
}()
var myMap map[int]int
myMap[0] = 100 //error 没有make
}
func main() {
go say()
go test()
for i := 1; i<=10;i++{
time.Sleep(time.Second)
fmt.Println("main ",i)
}
}