• go语言系列-从Goroutine到Channel


    Golang语言的核心特色

    Goroutine

    基本介绍

    进程和线程介绍

    1. 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位

    2. 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位

    3. 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行

    4. 一个程序至少有一个进程,一个进程至少有一个线程

    程序、进程和线程的关系示意图

    并发和并行

    1. 多线程程序在单核上运行,就是并发

    2. 多线程程序在多核上运行,就是并行

      并发:因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发

    并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行

    Go协程和Go主线程

    Go主线程(有程序员直接称为线程/也可以理解成进程):一个Go线程上,可以起多个协程,可以这样理解:协程是轻量级的线程【编译器做优化】

    Go协程的特点

    ​ 1) 有独立的栈空间

    ​ 2) 共享程序堆空间

    ​ 3) 调度由用户控制

    ​ 4) 协程是轻量级的线程

    快速入门

    案例说明

    编写一个程序,完成如下功能:

    ​ 1) 在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔一秒输出“hello,world”

    ​ 2) 在主线程中也每隔一秒输出“hello,world”,输出10次后,退出程序

    ​ 3) 要求主线程和goroutine同时执行

    画出主线程和协程执行流程图

    import (
       "fmt"
       "strconv"
       "time"
    )
    //编写一个函数/每隔一秒输出"hello,world"
    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,world" + strconv.Itoa(i))
          time.Sleep(time.Second)
       }
    }
    //main() hello,world1        //main主线程和test协程同时执行
    //test() hello,world1
    //main() hello,world2
    //test() hello,world2
    //......
    

    小结

    1. 主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源

    2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小

    3. Go的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制一般是基于线程的,开启过多的线程,资源耗费大,这里就突显了Go在并发上的优势了

    goroutine的调度模型

    1. M:操作系统的主线程(是物理线程)

    2. P:协程执行需要的上下文

    3. G:协程

    MPG模式运行的状态 -1

    1. 当前程序有三个M,如果三个M都在一个cpu上运行,就是并发,如果在不同的cpu上运行就是并行

    2. M1,M2,M3正在执行一个G,M1的协程队列有三个,M2的协程队列有三个,M3的协程队列有两个

    3. 从上图可以看到:Go的协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程

    4. 其它程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu

    MPG模式运行的状态 - 2

    1. 分成两个部分来看

    2. 原来的情况是MO主线程正在执行Go协程,另外有三个协程在队列等待

    3. 如果Go协程阻塞,比如读取文件或者数据库等

    4. 这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的Go仍然执行文件io的读写

    5. 这样的MPG调度模式,可以既让Go执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行

    6. 等到Go不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时Go又会被唤醒

    设置Go运行的CPU数

    为了充分利用多cpu的优势,在Go程序中,设置运行的cpu数目

    import (
       "fmt"
       "runtime"
    )
    
    func main()  {
       //获取当前系统cpu的数目
       num := runtime.NumCPU()
       //这里设置num - 1的cpu运行Go程序
       runtime.GOMAXPROCS(num - 1)
       fmt.Println("num = ", num)
    }
    
    Go1.8后,默认让程序运行在多核上,可以不用设置
    Go1.8前,还是要设置一下,可以更高效的利用cpu
    

    Channel(管道)

    看个需求

    需求:现在要计算1 - 200 的各个数的阶乘,并且把各个数的阶乘放入到map中,最后显示出来

    要求:使用goroutine

    分析思路

    ​ 1) 使用goroutine来完成,效率高,但是会出现并发/并行安全问题

    ​ 2) 这里就提出了不同goroutine如何通信的问题

    代码区

    ​ 1) 使用goroutine来完成(看看使用goroutine并发完成会出现什么问题?然后再去解决)

    ​ 2) 在运行某个程序时,如何知道是否存在资源竞争问题。方法很简单,在编译该程序时,增加一个参数 - race 即可

    示意图

    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
    ", i, v)
    	}
    }
    //fatal error: concurrent map writes
    //
    //goroutine 55 [running]:
    //runtime.throw(0x4d6d6d, 0x15)
    //	E:/GO/go/src/runtime/panic.go:774 +0x79 fp=0xc0000eff60 sp=0xc0000eff30 pc=0x42d229
    //runtime.mapassign_fast64(0x4b6240, 0xc00005c330, 0x31, 0x0)
    //	E:/GO/go/src/runtime/map_fast64.go:101 +0x357 fp=0xc0000effa0 sp=0xc0000eff60 pc=0x410167
    //main.test(0x31)
    //	E:/gostudent/src/2020-04-06/main.go:21 +0x6b fp=0xc0000effd8 sp=0xc0000effa0 pc=0x49c72b
    //runtime.goexit()
    //	E:/GO/go/src/runtime/asm_amd64.s:1357 +0x1 fp=0xc0000effe0 sp=0xc0000effd8 pc=0x4556a1
    //created by main.main
    //	E:/gostudent/src/2020-04-06/main.go:26 +0x5f
    //
    //goroutine 1 [runnable]:
    //time.Sleep(0x2540be400)
    //	E:/GO/go/src/runtime/time.go:84 +0x248
    //main.main()
    //	E:/gostudent/src/2020-04-06/main.go:29 +0x82
    

    不同goroutine之间如何通讯

    1. 全局变量的互斥锁

    2. 使用管道channel来解决

    使用全局变量加锁同步改进程序

    因为没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示concurrent map writes

    解决方案:加入互斥锁

    数的阶乘很大,结果会越界,可以将求阶乘改成sum += uint64(i)

    代码区改进

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    //思路
    //1. 编写一个函数,计算各个数的阶乘,并放入到map中
    //2. 启动的协程多个,统计的结果放入到map中
    //3. map应该做出一个全局的
    var (
    	myMap = make(map[uint]uint,10)
    	//声明一个全局的互斥锁
    	//lock 是一个全局的互斥锁
    	//sync 是包:synchornized 同步
    	//Mutex :是互斥
    	lock sync.Mutex
    )
    //test函数就是计算n!,将这个结果放入到myMap
    func test(n uint)  {
    	var res uint = 1
    	var i uint = 1
    	for ; i <= n; i++ {
    		res *= i
    	}
    	//这里将res 放入到myMap
    	//加锁
    	lock.Lock()
    	myMap[n] = res // concurrent map writes?
    	//解锁
    	lock.Unlock()
    }
    func main()  {
    	//这里开启多个协程完成这个任务[200个]
    	var i uint = 1
    	for ; i <= 200; i++ {
    		go test(i)
    	}
    	//休眠10秒钟【第二个问题】
    	time.Sleep(time.Second * 10)
    	//这里输出结果,遍历这个结果
    	lock.Lock()
    	for i, v := range myMap {
    		fmt.Printf("map[%d] = %d
    ", i, v)
    	}
    	lock.Unlock()
    }
    
    需求注意的是:uint64最大到20的阶乘,大整数可以使用math/big 来进行  实例:https://blog.csdn.net/hudmhacker/article/details/90081630
    

    为什么需要channel

    1. 前面使用全局变量加锁同步来解决goroutine的通讯,但不完美

    2. 主线程在等待所有gorountine全部完成的时间很难确定,这里设置了10秒,仅仅是估算

    3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁

    4. 通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作

    5. 上面种种分析都在呼唤一个新的通讯机制 - channel

    channel的基本介绍

    1. channel本质就是一个数据结构 - 队列

    2. 数据是先进先出【FIFO :first int first out】

    3. 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的

    4. channel有类型的,一个string的channel只能存放string类型数据

    定义/声明channel

    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

    ​ ....

    说明

    ​ 1) channel是引用类型

    ​ 2) channel必须初始化才能写入数据,即make后才能使用

    ​ 3) 管道是有类型的,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
    ", intChan, &intChan)
    	//3. 向管道写入数据
    	intChan <- 10
    	num := 211
    	intChan <- num
    	intChan <- 50
    	//intChan <- 99 //当给管道写入数据时,不能超过其容量
    	//4. 看看管道的长度和cap(容量)
    	fmt.Printf("channel len = %v cap = %v 
    ", len(intChan), cap(intChan))
    	//5. 从管道中读取数据
    	var num2 int
    	num2 = <- intChan
    	fmt.Println("num2 = ", num2)
    	fmt.Printf("channel len = %v cap = %v 
    ", len(intChan), cap(intChan))
    	//6. 在没有使用协程的情况下,如果管道数据已经全部取出,再取就会报告deadlock
    	num3 := <- intChan
    	num4 := <- intChan
    	num5 := <- intChan
    	fmt.Printf("num3 = %v num4 = %v num5 = %v ", num3, num4, num5)
    }
    //fatal error: all goroutines are asleep - deadlock!
    //intChan 的值 = 0xc000090000 intChan本身的地址 = 0xc00008a018
    //channel len = 3 cap = 3 
    //num2 =  10
    //channel len = 2 cap = 3 
    //
    //goroutine 1 [chan receive]:
    //main.main()
    //	E:/gostudent/src/2020-04-06/main.go:28 +0x4d4
    

    channel使用的注意事项

    1. channel 中只能存放指定的数据类型

    2. channel 的数据放满后,就不能再放入了

    3. 如果从channel取出数据后,可以继续放入

    4. 在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock

    读写channel案例演示

    1. 创建一个intChan,最多可以存放3个int,演示存3个数据到intChan,然后再取出这三个int
    func main()  {
       var intChan chan int
       intChan = make(chan  int, 3)
       intChan <- 10
       intChan <- 20
       intChan <- 10
       //因为intChan 的容量为3,再存放会报告deadlock
       //intChan <- 50
       num1 := <- intChan
       num2 := <- intChan
       num3 := <- intChan
       //因为intChan 这时已经没有数据了,再取会报告deadlock
       //num4 := <- intChan
       fmt.Printf("num1 = %v num2 = %v num3 = %v", num1, num2, num3)
    }
    //num1 = 10 num2 = 20 num3 = 10
    
    1. 创建一个mapChan,最多可以存放10个map[string]string的key-val,演示写入和读取
    func main() {
       var mapChan chan map[string]string
       mapChan = make(chan map[string]string, 2)
       m1 := make(map[string]string, 2)
       m1["city1"] = "北京"
       m1["city2"] = "天津"
       m2 := make(map[string]string, 2)
       m2["hero1"] = "宋江"
       m2["hero2"] = "林冲"
       mapChan <- m1
       mapChan <- m2
       num1 := <- mapChan
       num2 := <- mapChan
       fmt.Printf("num1 = %v num2 = %v", num1, num2)
    }
    //num1 = map[city1:北京 city2:天津] num2 = map[hero1:宋江 hero2:林冲]
    
    1. 创建一个catChan,最多可以存放10个Cat结构体变量,演示写入和读取的用法
    type Cat struct{
       Name string
       Age int
    }
    func main() {
       var catChan chan Cat
       catChan = make(chan Cat, 10)
       cat1 := Cat{Name: "tom", Age: 18,}
       cat2 := Cat{Name: "zise", Age: 18,}
       catChan <- cat1
       catChan <- cat2
       //取出
       cat11 := <- catChan
       cat22 := <- catChan
       fmt.Println(cat11, cat22)
    }
    //{tom 18} {zise 18}
    
    1. 创建一个catChan2,最多可以存放10个*Cat变量,演示写入和读取的用法
    type Cat struct{
       Name string
       Age int
    }
    func main() {
       var catChan chan *Cat
       catChan = make(chan *Cat, 10)
       cat1 := Cat{Name: "tom", Age: 18,}
       cat2 := Cat{Name: "zise", Age: 18,}
       catChan <- &cat1
       catChan <- &cat2
       //取出
       cat11 := <- catChan
       cat22 := <- catChan
       fmt.Println(*cat11, *cat22)
    }
    //{tom 18} {zise 18}
    
    1. 创建一个allChan,最多可以存放10个任意数据类型变量,演示写入和读取的用法
    type Cat struct {
       Name string
       Age int
    }
    
    func main()  {
       var allChan chan interface{}
       allChan = make(chan interface{}, 10)
       cat1 := Cat{Name: "tom", Age: 18}
       cat2 := Cat{Name: "zise", Age: 18}
       allChan <- cat1
       allChan <- cat2
       allChan <- 10
       allChan <- "jack"
       //取出
       cat11 := <- allChan
       cat22 := <- allChan
       v1 := <- allChan
       v2 := <- allChan
       fmt.Println(cat11, cat22, v1, v2)
    }
    //{tom 18} {zise 18} 10 jack
    
    1. 看下面的代码,会输出什么
    type Cat struct {
       Name string
       Age int
    }
    
    func main()  {
       var allChan chan interface{}
       allChan = make(chan interface{}, 10)
       cat1 := Cat{Name: "tom", Age: 18}
       cat2 := Cat{Name: "zise", Age: 18}
       allChan <- cat1
       allChan <- cat2
       allChan <- 10
       allChan <- "jack"
       //取出
       //cat11 := <- allChan
       //fmt.Println(cat11.Name)
       // # command-line-arguments
       //srcgo_codechapter15exec03	est03.go:23:19: cat11.Name undefined (type interface {} is interface with no methods)
       newCat := <- allChan //从管道中取出的Cat是什么
       fmt.Printf("newCat = %T newCat = %v 
    ", newCat, newCat)
       //下面写法是错误的,编译不通过
       //fmt.Printf("newCat.Name = %v", newCat.Name)
       //使用类型断言
       a := newCat.(Cat)
       fmt.Printf("newCat.Name = %v", a.Name)
    }
    //newCat = main.Cat newCat = {tom 18} 
    //newCat.Name = tom
    

    channel的遍历和关闭

    channel的关闭

    使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据

    func main()  {
       intChan := make(chan int, 3)
       intChan <- 100
       intChan <- 200
       close(intChan) //close
       //这时不能够再写入数到channel
       //intChan <- 300
       fmt.Println("oko")
       //当管道关闭后,读取数据是可以的
       n1 := <- intChan
       fmt.Println("n1 = ", n1)
    }
    //oko
    //n1 =  100
    

    channel的遍历

    channel支持 for - range 的方式进行遍历,注意两个细节

    1. 在遍历时,如果channel没有关闭,则会出现deadlock的错误

    2. 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历

    channel遍历和关闭的案例演示

    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++ {
       //
       //}
       //1)在遍历时,如果channel没有关闭,则会出现deadlock的错误
       //2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
       close(intChan2)
       for v := range intChan2 {
          fmt.Println("v = ", v)
       }
    }
    

    应用案例

    应用案例-利于管道实现边写边读

    请完成goroutine和channel协同工作的案例,具体要求:

    1. 开启一个writeData协程,向管道intChan中写入50个整数

    2. 开启一个readData协程,从管道intChan中读取writeData写入的数据

    3. 注意:writeData和readData操作的是同一个管道

    4. 主线程需要等待writeData和readData协程都完成工作才能退出【管道】

    思路分析

    代码区

    import (
       "fmt"
       "time"
    )
    //writeData
    func writeData(intChan chan int)  {
       for i := 1; i <= 50; i++ {
          //放入数据
          intChan <- i
          fmt.Println("writeData", i)
          time.Sleep(time.Second)
       }
       close(intChan) //关闭
    }
    //readData
    func readData(intChan chan int, exitChan chan bool)  {
       for {
          v, ok := <- intChan
          if !ok {
             break
          }
          time.Sleep(time.Second)
          fmt.Printf("readData 读到数据 = %v
    ", v)
       }
       //readData 读取完数据后,即任务完成
       exitChan <- true
       close(exitChan)
    }
    func main()  {
       //创建两个管道
       intChan := make(chan int, 50)
       exitChan := make(chan bool, 1)
       go writeData(intChan)
       go readData(intChan, exitChan)
       time.Sleep(time.Second * 10)
       for {
          _,ok := <- exitChan
          if !ok {
             break
          }
       }
    }
    

    var (
        myMap = make(map[int]int, 10)
    )
    
    func cal(n int) map[int]int {
        res := 1
        for i := 1; i <= n; i++ {
            res *= i
        }
        myMap[n] = res
        return myMap
    }
    
    func write(myChan chan map[int]int) {
        for i := 0; i <= 15; i++ {
            myChan <- cal(i)
            fmt.Println("writer data:", cal(i))
        }
        close(myChan)
    }
    
    func read(myChan chan map[int]int, exitChan chan bool) {
        for {
            v, ok := <-myChan
            if !ok {
                break
            }
            fmt.Println("read data:", v)
        }
        exitChan <- true
        close(exitChan)
    }
    
    func main() {
        var myChan chan map[int]int
        myChan = make(chan map[int]int, 20)
        var exitChan chan bool
        exitChan = make(chan bool, 1)
        go write(myChan)
        go read(myChan, exitChan)
        for {
            _, ok := <-exitChan
            if !ok {
                break
            }
        }
    }
    

    应用案例 - 阻塞


    思考:假设我们注销掉go read(myChan,exitChan)会发生什么呢?

    也就是说,只有写入myChan而没有读取myChan,当存入myChan里面的数据达到了myChan的容量,再继续存入就会报deadlock错误。同时,由于exitChan需要写入一个true,而exitChan需要读取完myChan中的数据后才写入一个true,但是现在不能进行读取,也就是说,true不会写入exitChan,就形成了阻塞。假设我们打开go read(myChan,exitChan),我们设置其每隔1秒才读取一条数据,而写入则让其正常运行,也就是说,写入很快,读取很慢,这样会导致deadlock吗?答案是不会,只要有读取,golang会有个机制,不会让myChan存储的值超过myChan的容量。

    应用案例-求素数

    需求

    ​ 要求统计 1 - 8000的数字中,哪些是素数?

    ​ 现在具备了goroutine和channel的知识后,就可以完成了

    分析思路

    ​ 传统的方法:使用一个循环,循环的判断各个数是不是素数

    ​ 使用并发/并行的方式:将统计素数的任务分配给多个(4个)goroutine去完成,完成任务时间短

    画出分析思路

    说明:有五个协程,三个管道。其中一个协程用于写入数字到intChan管道中,另外四个用于取出intChan管道中的数字并判断是否是素数,然后将素数写入到primeChan管道中,最后如果后面四个协程哪一个工作完了,就写入一个true到exit管道中,最后利用循环判断这四个协程是否都完成任务,并退出

    package main
    
    import (
    	"fmt"
    	"time"
    )
    //向intChan放入1 - 8000个数
    func putNum(intChan chan int)  {
    	for i:= 1; i <= 8000; 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
    		if !ok { //intChan 娶不到..
    			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, 200000)
    	primeChan := make(chan int, 200000) //放入结果
    	//标识退出的管道
    	exitChan := make(chan bool, 4) // 4个
    	//开启一个协程,向intChan放入1 - 200000个数
    	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
    		}
    		//当从exitChan 取出4个结果
    		//就可以关闭prprimeChan
    		close(primeChan)
    	}()
    	//遍历primeChan,把结果取出
    	for {
    		res, ok := <- primeChan
    		if !ok {
    			break
    		}
    		//将结果输出
    		fmt.Printf("素数 = %d
    ", res)
    	}
    	fmt.Println("main线程退出")
    }
    

    升级

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func isPrime(n int) bool {
    	for i := 2; i <= n; i++ {
    		if n%i == 0 {
    			return false
    		}
    	}
    	return true
    }
    
    //传统方法耗时
    func Test() {
    	start := time.Now()
    	for i := 1; i < 80000; i++ {
    		isPrime(i)
    	}
    	cost := time.Since(start)
    	fmt.Printf("传统方法消耗时间为:%s", cost)
    }
    
    //向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
    		if !ok { //intChan 娶不到..
    			break
    		}
    		//flag = true //假设是素数
    		//判断num是不是素数
    	//	for i := 2; i < num; i++ {
    	//		if num % i == 0 { //说明该num 不是素数
    	//			flag = false
    	//			break
    	//		}
    	//	}
    	//	if flag {
    	//		//将这个数就放入到primeChan
    	//		primeChan <- num
    	//	}
    	//}
    		isp := isPrime(num)
    		if !isp {
    			continue
    		} else {
    			primeChan <- num
    		}
    	}
    	fmt.Println("有一个primeNum协程因为取不到数据,退出")
    	//这里还不能关闭primeChan
    	//向exitChan 写入true
    	exitChan <- true
    }
    func main()  {
    	intChan := make(chan int, 200000)
    	primeChan := make(chan int, 200000) //放入结果
    	//标识退出的管道
    	exitChan := make(chan bool, 4) // 4个
    	//记录当前时间
    	start := time.Now()
    	//开启一个协程,向intChan放入1 - 200000个数
    	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
    		}
    		//当从exitChan 取出4个结果
    		//就可以关闭prprimeChan
    		//计算耗时时间
    		cost := time.Since(start)
    		fmt.Printf("使用协程耗费时间:%s
    ", cost)
    		close(primeChan)
    	}()
    	//遍历primeChan,把结果取出
    	for {
    		_, ok := <- primeChan
    		if !ok {
    			break
    		}
    		//将结果输出
    		//fmt.Printf("素数 = %d
    ", res)
    	}
    	fmt.Println("main线程退出")
    	Test()
    }
    //有一个primeNum协程因为取不到数据,退出
    //有一个primeNum协程因为取不到数据,退出
    //有一个primeNum协程因为取不到数据,退出
    //有一个primeNum协程因为取不到数据,退出
    //使用协程耗费时间:876.6558ms
    //main线程退出
    //传统方法消耗时间为:3.3300976s
    

    channel使用细节和注意事项

    channel可以声明为只读,或者只写性质

    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)
    }
    

    channel只读和只写的最佳实践案例

    //ch chan <- int 这样ch就只能写操作了
    func send(ch chan <- int, exitChan chan struct{})  {
       for i := 0; i < 10; i++ {
          ch <- i
       }
       close(ch)
       var a struct{}
       exitChan <- a
    }
    //ch <- chan int ,这样ch 就只能读操作了
    func recv(ch <- chan int, exitChan chan struct{})  {
       for {
          v, ok := <- ch
          if !ok {
             break
          }
          fmt.Println(v)
       }
       var a struct{}
       exitChan <- a
    }
    
    func main()  {
       var ch chan  int
       ch = make(chan int, 10)
       exitChan := make(chan struct{}, 2)
       go send(ch, exitChan)
       go recv(ch, exitChan)
       var total = 0
       for _ = range exitChan {
          total ++
          if total == 2 {
             break
          }
       }
       fmt.Println("结束...")
    }
    

    使用select可以解决从管道取数据的阻塞问题

    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
    ", v)
             time.Sleep(time.Second)
          case v := <-stringChan:
             fmt.Printf("从stringChan读取的数据%s
    ", v)
             time.Sleep(time.Second)
          default:
             fmt.Printf("都取不到了,不玩了,程序员可以加入逻辑
    ")
             time.Sleep(time.Second)
             return
             //break label
          }
       }
    }
    

    goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题

    说明:如果起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成整个程序崩溃,这时我们可以在goroutine中使用recover来捕获panic,进行处理,这样即使这个协程发生了问题,但是主线程仍然不受影响,可以继续执行。

    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)
       }
    }
    //main() ok= 0
    //test() 发生错误 assignment to entry in nil map
    //hello,world
    //main() ok= 1
    //hello,world
    //main() ok= 2
    //hello,world
    //main() ok= 3
    //hello,world
    //main() ok= 4
    //hello,world
    //main() ok= 5
    //hello,world
    //main() ok= 6
    //hello,world
    //main() ok= 7
    //hello,world
    //main() ok= 8
    //hello,world
    //main() ok= 9
    //hello,world
    

    管道的练习题

    说明:

    1. 创建一个Person结构体[Name,Age,Address]

    2. 使用rand方法配合随机创建10个Person实例,并放入到channel中

    3. 遍历channel,将各个Person实例的信息显示在终端...

  • 相关阅读:
    考研打卡_Day018
    如何使用python中的pymysql操作mysql数据库
    Linux系统目录结构和常用目录主要存放内容的说明
    MySQL基础入门使用和命令的使用
    Python中property属性的概论和使用方法
    如何有效的优化自己的网站访问速度
    机器学习中的特征工程学习
    ffmpeg中c语言sdk多媒体互转主要使用的api
    FFmpeg使用c语言sdk实现打印视频的信息
    ffmpeg使用C语言sdk实现抽取视频中的视频数据
  • 原文地址:https://www.cnblogs.com/zisefeizhu/p/12643838.html
Copyright © 2020-2023  润新知