goroutine 和 channel
goroutine
1、进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
2、线程是进程中的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
3、一个进程能够创建和销毁多个线程,同一个进程中的多个线程可以并发执行
4、一个程序至少有一个进程,一个进程至少有一个线程
程序、进程和线程的关系示意图:
并发和并行
1、多线程程序在单核上运行,就是并发
2、多线程程序在多核上运行,就是并行
3、示意图:
小结:
并发:因为是在一个cpu上,比如有10个线程,每个线程执行10ms(进行轮询操作),从人的角度来看,好像这10个线程都在运行,但是从微观角度来看,在某一个时间点,其实只有一个线程在执行,这就是并发
并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10ms(各自在不同的cpu上执行),从人的角度来看,这10个线程都在运行,但是从微观来看,在某一个时间点,也同时有10个线程在执行,这就是并行
Go协程和 Go 主线程
1、有独立的栈空间
2、共享程序堆空间
3、调度由用户控制
4、协程是轻量级的线程
一个示意图:
goroutine 快速入门
代码如下:
package main import ( "fmt" "strconv" "time" ) func test() { for i := 1; i <= 10; i++ { fmt.Println("test () hello,world,bingle " + strconv.Itoa(i)) time.Sleep(time.Second) } } func main() { go test() // 开启了一个协程 for i := 1; i <= 10; i++ { fmt.Println(" main() hello,golang and bingle" + strconv.Itoa(i)) time.Sleep(time.Second) } }
执行结果如下:输出的效果说明, main 这个主线程和 test 协程同时执行.
主线程和协程执行流程图
快速入门小结:
1、主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
2、协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
3、Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势了
goroutine 的调度模型
1、M:操作系统的主线程(是物理线程)
2、P:协程执行需要的上下文
3、G:协程
1、当前程序有三个M,如果三个M都在一个cpu上运行,就是并发,如果在不同的cpu运行就是并行
2、M1,M2,M3正在执行一个G,M1的协程队列有3个,M2的协程队列有3个,M3的协程队列有2个
3、从上图可以看到,Go的协程是轻量级的线程,是逻辑态的,Go可以容易的开启上万个协程
4、其他程序C/java/C#的多线程,往往是内核态的,比较重量级,几千个线程可能耗光CPU
1、go1.8后,默认让程序运行在多个核上,可以不用设置了
2、go1.8前,还是要设置一下,可以更高效的利用cpu
package main import ( "fmt" "time" ) // 思路 // 1. 编写一个函数,来计算各个数的阶乘,并放入到 map 中. // 2. 我们启动的协程多个,统计的将结果放入到 map 中 // 3. map 应该做出一个全局的. var ( myMap = make(map[int]int, 10) ) // test 函数就是计算 n!, 让将这个结果放入到 myMap func test(n int) { res := 1 for i := 1; i <= n; i++ { res *= i } //这里我们将 res 放入到 myMap myMap[n] = res //concurrent map writes? } 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]=%d\n", i, v) } }
4、示意图:
1、全局变量的互斥锁
2、使用管道 channel 来解决
为什么需要 channel
1、channle 本质就是一个数据结构-队列
2、数据是先进先出【FIFO : first in first out】
3、线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
4、channel 有类型的,一个 string 的 channel 只能存放 string 类型数据
5、示意图:
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" func main() { // 创建一个intChan,最多可以放3个int,演示存3数据到intChan,然后再取出三个int var intChan chan int intChan = make(chan int,3) intChan <- 10 intChan <- 20 intChan <- 10 // 因为 intChan 的容量为3,再存放数据就会报deadlock //intChan <- 100 num1 := <- intChan num2 := <- intChan num3 := <- intChan // 因为 intChan 这时已经没有数据了,再取就会报deadlock //num4 := <- intChan fmt.Printf("num1 = %v,num2 = %v,num3 = %v",num1,num2,num3) }
2、创建一个mapChan,最多可以放10个map[string]string的key-value,演示写入和读取
package main import "fmt" type Person struct { Name string Age int } func main() { // 创建一个personChan,最多可以存放10个Person结构体变量,演示写入和读取的用法 var personChan chan Person personChan = make(chan Person, 10) person1 := Person{Name:"bingle1",Age:18} person2 := Person{Name:"bingle2",Age:20} personChan <- person1 personChan <- person2 //取出 person11 := <- personChan person22 := <- personChan fmt.Println(person11,person22) }
4、创建一个personChan,最多可以存放10个*Person结构体变量,演示写入和读取的用法
package main import "fmt" type Person struct { Name string Age int } func main() { // 创建一个personChan,最多可以存放10个*Person结构体变量,演示写入和读取的用法 var personChan chan *Person personChan = make(chan *Person, 10) person1 := Person{Name:"bingle1",Age:18} person2 := Person{Name:"bingle2",Age:20} personChan <- &person1 personChan <- &person2 //取出 person11 := <- personChan person22 := <- personChan fmt.Println(person11,person22) }
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) }
package main import "fmt" func main() { //遍历管道 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) } }
应用实例1
代码实现:
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 } } }
应用实例2-阻塞
应用实例 3
package main import ( "fmt" "time" ) //向 intChan放入 1-80000个数 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线程退出") }
2、使用 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 } } }
3、goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题
如果我们起了一个协程,但是这个协程出现了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) } }