goroutine-看一个需求
需求:要求统计 1-9000000000 的数字中,哪些是素数?程序员
分析思路:
1) 传统的方法,就是使用一个循环,循环的判断各个数是否是素数。[很慢]
2) 使用并发或者并行的方式,将统计素数的任务分配给多个 goroutine 去完成,这时就会使用到goroutine.【速度提升 4 倍】golang
进程和线程介绍编程
程序、进程和线程的关系安全
并发和并行
1) 多线程程序在单核上运行,就是并发
2) 多线程程序在多核上运行,就是并行数据结构
Go 主线程(有程序员直接称为线程/也能够理解成进程): 一个 Go 线程上,能够起多个协程,你能够这样理解,协程是轻量级的线程[编译器作优化]。多线程
Go 协程的特色
1) 有独立的栈空间
2) 共享程序堆空间
3) 调度由用户控制
4) 协程是轻量级的线程并发
案例说明
请编写一个程序,完成以下功能:
1) 在主线程(能够理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出 "hello,world"
2) 在主线程中也每隔一秒输出"hello,golang", 输出 10 次后,退出程序
3) 要求主线程和 goroutine 同时执行.编程语言
package main import ( "fmt" "strconv" "time" ) // 在主线程(能够理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 "hello,world" // 在主线程中也每隔一秒输出"hello,golang", 输出10次后,退出程序 // 要求主线程和goroutine同时执行 //编写一个函数,每隔1秒输出 "hello,world" func test() { for i := 1; i <= 10; i++ { fmt.Println("tesst () 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) } }
1) 主线程是一个物理线程,直接做用在 cpu 上的。是重量级的,很是耗费 cpu 资源。
2) 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
3) Golang 的协程机制是重要的特色,能够轻松的 开启上万个协程。其它编程语言的并发机制是通常基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优点了函数
解释一下MPG含义:
M(Machine):操做系统的主线程
P(Processor):协程执行须要的资源(上下文context),能够看做一个局部的调度器,使go代码在一个线程上跑,他是实现从N:1到N:M映射的关键
G(Gorountine):协程,有本身的栈。包含指令指针(instruction pointer)和其它信息(正在等待的channel等等),用于调度。一个P下面能够有多个G测试
P的数量能够经过GOMAXPROCS()来设置,他其实表明了真正的并发度,即有多少个goroutine能够同时运行。P同时也维护着G(协程)的队列(称之为runqueue进程队列)。Go代码中的M每有一个语句被执行,P就在末尾加入一个G(从runqueue队列中取出来的),在下一个调度点(P),就从runqueue队列中取出G。
P能够在OS线程(主线程,或者是M)被阻塞时,转到另外一个OS线程(M)!Go中的调度器保证有足够的线程来运行全部的P。当启用一个M0中的G0被sysCall(系统调用)的时候,M0下面的P转给另外一个线程M1(能够是建立的,也能够是本来就存在的)。M1接受了P(包括P所带的runqueue的队列里面全部状态的G,但不包括已经被syscall的G0),继续运行。而M0会等待执行syscall的G0的返回值。当G0的syscall结束后,他的主线程M0会尝试取得一个P来运行G0,通常状况下,他会从其余的M里面偷一个P过来,若是没有偷到的话就会把G0放到一个Global runqueue(全局进程队列)中,而后把本身(M0)放进线程池或者转为休眠状态。
介绍:为了充分了利用多 cpu 的优点,在 Golang 程序中,设置运行的 cpu 数目
package main import ( "runtime" "fmt" ) func main() { cpuNum := runtime.NumCPU() fmt.Println("cpuNum=", cpuNum) //能够本身设置使用多个cpu runtime.GOMAXPROCS(cpuNum - 1) fmt.Println("ok") }
需求:如今要计算 1-200 的各个数的阶乘,而且把各个数的阶乘放入到 map 中。最后显示出来。
要求使用 goroutine 完成
分析思路:
1) 使用 goroutine 来完成,效率高,可是会出现并发/并行安全问题.
2) 这里就提出了不一样 goroutine 如何通讯的问题
代码实现
1) 使用 goroutine 来完成(看看使用 gorotine 并发完成会出现什么问题? 而后去解决)
2) 在运行某个程序时,如何知道是否存在资源竞争问题。 方法很简单,在编译该程序时,增长一个参数 -race 便可
1) 全局变量的互斥锁
2) 使用管道 channel 来解决
由于没有对全局变量 m 加锁,所以会出现资源争夺问题,代码会出现错误,提示 concurrent map
writes
解决方案:加入互斥锁
咱们的数的阶乘很大,结果会越界,能够将求阶乘改为 sum += uint64(i)
package main import ( "fmt" _ "time" "sync" ) // 需求:如今要计算 1-200 的各个数的阶乘,而且把各个数的阶乘放入到map中。 // 最后显示出来。要求使用goroutine完成 // 思路 // 1. 编写一个函数,来计算各个数的阶乘,并放入到 map中. // 2. 咱们启动的协程多个,统计的将结果放入到 map中 // 3. map 应该作出一个全局的. var ( myMap = make(map[int]int, 10) //声明一个全局的互斥锁 //lock 是一个全局的互斥锁, //sync 是包: synchornized 同步 //Mutex : 是互斥 lock sync.Mutex ) // test 函数就是计算 n!, 让将这个结果放入到 myMap func test(n int) { res := 1 for i := 1; i <= n; i++ { res *= i } //这里咱们将 res 放入到myMap //加锁 lock.Lock() myMap[n] = res //concurrent map writes? //解锁 lock.Unlock() } func main() { // 咱们这里开启多个协程完成这个任务[200个] for i := 1; i <= 20; i++ { go test(i) } //休眠10秒钟【第二个问题 】 //time.Sleep(time.Second * 5) //这里咱们输出结果,变量这个结果 lock.Lock() for i, v := range myMap { fmt.Printf("map[%d]=%d\n", i, v) } lock.Unlock() }
1) 前面使用全局变量加锁同步来解决 goroutine 的通信,但不完美
2) 主线程在等待全部 goroutine 所有完成的时间很难肯定,咱们这里设置 10 秒,仅仅是估算。
3) 若是主线程休眠时间长了,会加长等待时间,若是等待时间短了,可能还有 goroutine 处于工做
状态,这时也会随主线程的退出而销毁
4) 经过全局变量加锁同步来实现通信,也并不利用多个协程对全局变量的读写操做。
5) 上面种种分析都在呼唤一个新的通信机制-channel
1) channle 本质就是一个数据结构-队列
2) 数据是先进先出【FIFO : first in first out】
3) 线程安全,多 goroutine 访问时,不须要加锁,就是说 channel 自己就是线程安全的
4) channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
var 变量名 chan 数据类型
举例:
var intChan chan int (intChan 用于存放 int 数据) var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型) var perChan chan Person var perChan2 chan *Person ...
说明
channel 是引用类型
channel 必须初始化才能写入数据, 即 make 后才能使用
管道是有类型的,intChan 只能写入 整数 int
package main import ( "fmt" ) func main() { //演示一下管道的使用 //1. 建立一个能够存放3个int类型的管道 var intChan chan int intChan = make(chan int, 3) //2. 看看intChan是什么 fmt.Printf("intChan 的值=%v intChan自己的地址=%p\n", intChan, &intChan) //3. 向管道写入数据 intChan<- 10 num := 211 intChan<- num intChan<- 50 // //若是从channel取出数据后,能够继续放入 <-intChan intChan<- 98//注意点, 当咱们给管写入数据时,不能超过其容量 //4. 看看管道的长度和cap(容量) fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3 //5. 从管道中读取数据 var num2 int num2 = <-intChan fmt.Println("num2=", num2) fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 2, 3 //6. 在没有使用协程的状况下,若是咱们的管道数据已经所有取出,再取就会报告 deadlock num3 := <-intChan num4 := <-intChan //num5 := <-intChan fmt.Println("num3=", num3, "num4=", num4/*, "num5=", num5*/) }
1) channel 中只能存放指定的数据类型
2) channle 的数据放满后,就不能再放入了
3) 若是从 channel 取出数据后,能够继续放入
4) 在没有使用协程的状况下,若是 channel 数据取完了,再取,就会报 dead lock
package main import ( "fmt" ) type Cat struct { Name string Age int } func main() { //定义一个存听任意数据类型的管道 3个数据 //var allChan chan interface{} allChan := make(chan interface{}, 3) allChan<- 10 allChan<- "tom jack" cat := Cat{"小花猫", 4} allChan<- cat //咱们但愿得到到管道中的第三个元素,则先将前2个推出 <-allChan <-allChan newCat := <-allChan //从管道中取出的Cat是什么? fmt.Printf("newCat=%T , newCat=%v\n", newCat, newCat) //下面的写法是错误的!编译不经过 //fmt.Printf("newCat.Name=%v", newCat.Name) //使用类型断言 a := newCat.(Cat) fmt.Printf("newCat.Name=%v", a.Name) }
使用内置函数 close 能够关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,可是仍然能够从该 channel 读取数据
channel 支持 for--range 的方式进行遍历,请注意两个细节
1) 在遍历时,若是 channel 没有关闭,则回出现 deadlock 的错误
2) 在遍历时,若是 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
channel 遍历和关闭的案例演示
package main import ( "fmt" ) func main() { intChan := make(chan int, 3) intChan<- 100 intChan<- 200 close(intChan) // close //这是不可以再写入数到channel //intChan<- 300 fmt.Println("okook~") //当管道关闭后,读取数据是能够的 n1 := <-intChan fmt.Println("n1=", n1) //遍历管道 intChan2 := make(chan int, 100) for i := 0; i < 100; i++ { intChan2<- i * 2 //放入100个数据到管道 } //遍历管道不能使用普通的 for 循环 // for i := 0; i < len(intChan2); i++ { // } //在遍历时,若是channel没有关闭,则会出现deadlock的错误 //在遍历时,若是channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历 close(intChan2) for v := range intChan2 { fmt.Println("v=", v) } }
package main import ( "fmt" "time" ) //write Data func writeData(intChan chan int) { for i := 1; i <= 50; i++ { //放入数据 intChan<- i // fmt.Println("writeData ", i) //time.Sleep(time.Second) } close(intChan) //关闭 } //read data func readData(intChan chan int, exitChan chan bool) { for { v, ok := <-intChan if !ok { break } time.Sleep(time.Second) fmt.Printf("readData 读到数据=%v\n", v) } //readData 读取完数据后,即任务完成 exitChan<- true close(exitChan) } func main() { //建立两个管道 intChan := make(chan int, 10) exitChan := make(chan bool, 1) go writeData(intChan) go readData(intChan, exitChan) time.Sleep(time.Second * 10) for { _, ok := <-exitChan if !ok { break } } }
若上面的代码,注释掉go readData(intChan, exitChan)
,会怎样,由于管道有长度,因此当编译器发现一个管道只有写而没有读,改管道会阻塞(读与写的频率不一致不要紧)!
需求:
要求统计 1-200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,如今咱们有 goroutine和 channel 的知识后,就能够完成了 [测试数据: 80000]
分析思路:
传统的方法,就是使用一个循环,循环的判断各个数是否是素数【ok】。
使用并发/并行的方式,将统计素数的任务分配给多个(4 个)goroutine 去完成,完成任务时间短。
传统方法,一个协程
package main import ( "time" "fmt" ) func main() { start := time.Now().Unix() for num := 1; num <= 80000; num++ { flag := true //假设是素数 //判断num是否是素数 for i := 2; i < num; i++ { if num % i == 0 {//说明该num不是素数 flag = false break } } if flag { //将这个数就放入到primeChan //primeChan<- num } } end := time.Now().Unix() fmt.Println("普通的方法耗时=", end - start) }
开了四个协程
package main import ( "fmt" "time" ) //向 intChan放入 1-8000个数 func putNum(intChan chan int) { for i := 1; i <= 80000; i++ { intChan<- i } //关闭intChan close(intChan) } // 从 intChan取出数据,并判断是否为素数,若是是,就 // //放入到primeChan func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) { //使用for 循环 // var num int var flag bool // for { //time.Sleep(time.Millisecond * 10) num, ok := <-intChan //intChan 取不到.. if !ok { break } flag = true //假设是素数 //判断num是否是素数 for i := 2; i < num; i++ { if num % i == 0 {//说明该num不是素数 flag = false break } } if flag { //将这个数就放入到primeChan primeChan<- num } } fmt.Println("有一个primeNum 协程由于取不到数据,退出") //这里咱们还不能关闭 primeChan //向 exitChan 写入true exitChan<- true } func main() { intChan := make(chan int , 1000) primeChan := make(chan int, 20000)//放入结果 //标识退出的管道 exitChan := make(chan bool, 8) // 4个 start := time.Now().Unix() //开启一个协程,向 intChan放入 1-8000个数 go putNum(intChan) //开启4个协程,从 intChan取出数据,并判断是否为素数,若是是,就 //放入到primeChan for i := 0; i < 8; i++ { go primeNum(intChan, primeChan, exitChan) } //这里咱们主线程,进行处理 //直接 go func(){ for i := 0; i < 8; i++ { <-exitChan } end := time.Now().Unix() fmt.Println("使用协程耗时=", end - start) //当咱们从exitChan 取出了4个结果,就能够放心的关闭 prprimeChan close(primeChan) }() //遍历咱们的 primeChan ,把结果取出 for { _, ok := <-primeChan if !ok{ break } //将结果输出 //fmt.Printf("素数=%d\n", res) } fmt.Println("main线程退出") }
结论:使用 go 协程后,执行的速度,理论上比普通方法提升至少 4 倍(我这是两倍)
1) channel 能够声明为只读,或者只写性质 【案例演示】
package main import ( "fmt" ) func main() { //管道能够声明为只读或者只写 //1. 在默认状况下下,管道是双向 //var chan1 chan int //可读可写 //2 声明为只写 var chan2 chan<- int chan2 = make(chan int, 3) chan2<- 20 //num := <-chan2 //error fmt.Println("chan2=", chan2) //3. 声明为只读 var chan3 <-chan int num2 := <-chan3 //chan3<- 30 //err fmt.Println("num2", num2) }
3) 使用 select 能够解决从管道取数据的阻塞问题
package main import ( "fmt" "time" ) func main() { //使用select能够解决从管道取数据的阻塞问题 //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" + fmt.Sprintf("%d", i) } //传统的方法在遍历管道时,若是不关闭会阻塞而致使 deadlock //问题,在实际开发中,可能咱们很差肯定什么关闭该管道. //可使用select 方式能够解决 //label: for { select { //注意: 这里,若是intChan一直没有关闭,不会一直阻塞而deadlock //,会自动到下一个case匹配 case v := <-intChan : fmt.Printf("从intChan读取的数据%d\n", v) time.Sleep(time.Second) case v := <-stringChan : fmt.Printf("从stringChan读取的数据%s\n", v) time.Sleep(time.Second) default : fmt.Printf("都取不到了,不玩了, 程序员能够加入逻辑\n") time.Sleep(time.Second) return //break label } } }
4) goroutine 中使用 recover,解决协程中出现 panic,致使程序崩溃问题
若是咱们开了一个协程,但这个协程出现panic,就会致使整个程序崩溃,这时咱们能够在goroutine中使用recover来捕获panic,这样及时协程发生问题,主线程依然不受影响
package main import ( "fmt" "time" ) //函数 func sayHello() { for i := 0; i < 10; i++ { time.Sleep(time.Second) fmt.Println("hello,world") } } //函数 func test() { //这里咱们可使用defer + recover defer func() { //捕获test抛出的panic if err := recover(); err != nil { fmt.Println("test() 发生错误", err) } }() //定义了一个map var myMap map[int]string myMap[0] = "golang" //error } func main() { go sayHello() go test() for i := 0; i < 10; i++ { fmt.Println("main() ok=", i) time.Sleep(time.Second) } }