Go - 39 Go 协程+管道

进程和线程的说明
    1.进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。    
    2.线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
    3.一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行。
    4.一个程序至少有一个进程,一个进程至少有一个线程。
 
 
并发和并行
    1.多线程程序在单核上运行,就是并发。
    并发
        因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
    2.多线程程序在多核上运行,就是并行。
    并行
        因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行。
 
 
Go协程和Go主线程
    1.Go主线程(也有直接称为线程/进程):一个Go线程上,可以起上万个协程,协程是轻量级的线程(底层是编译器做了优化)。
    2.Go协程的特点:
        有独立的栈空间
        共享程序堆空间
        调度由用户控制
        协程是轻量级的线程
    
    快速案例:
        package main 
        import (     "fmt"     "strconv"     "time" ) 
        func g_test() {     
                for i:=0; i < 100; i ++ {         
                        fmt.Println("g_test() hello,world " + strconv.Itoa(i))         
                        // 隔一秒         
                        time.Sleep(time.Second)     
                } 
        } 
        func main() {     
 
                go g_test()  // 开启一个协程    
 
                for i:=0; i < 10; i ++ {         
                        fmt.Println("main() hello,golang " + strconv.Itoa(i))         
                        time.Sleep(time.Second)     
                } 
        }
        案例小结:
            1.主线程是一个物理线程,直接作用在cpu上,是重量级的,非常耗费cpu资源。
            2.协程从主线程开启的,是轻量级的线程,是逻辑态,对资源耗费相对小。
            3.Golang的协程机制是重要的特点,可以轻松的开启上万个协程,其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了。
 
 
goroutine的调度模型
    MPG模式基本介绍:
        M:操作系统的主线程(是物理线程)
        P:协程执行需要的上下文
        G:协程
 
 
Golang设置运行的cpu个数
    代码实现:
        package main 
        import (     "fmt"     "runtime" ) 
        func main() {     
                cpuNum := runtime.NumCPU()     
                fmt.Println("win cpuNum=", cpuNum)     
                // 可以自己设置使用几个cpu     
                res := runtime.GOMAXPROCS(cpuNum-2)     
                fmt.Println("ok", res) 
        }
        go1.8后,默认让程序运行在多个核上,可以不用设置了。
        go1.8前,还是要设置一下的,可以更高效的利用cpu
 
 
全局变量互斥锁解决资源竞争
    协程并发(并行)出现资源竞争问题:
        运行时增加 -race 参数,确实会发现有资源竞争问题(先编译:go build -race  main.go  然后执行main.exe文件,会出现资源竞争问题)
        sync包里的Mutex 里面的 Lock 和Unlock 方法来解决
        实际案例:
            package main 
            import (     "fmt"     "time"     "sync" ) 
            // 全局互斥锁解决 资源竞争问题 
            var (     
                testMap = make(map[int]int)  
   
                // 声明一个全局互斥锁     
                // lock 是一个全局的互斥锁     
                // sync 是包 :synchornized 同步     
                // Mutex:互斥     
                lock sync.Mutex 
            ) 
            func jc_test(n int) {     
                    res := 1     
                    for i := 1; i <= n; i ++ {         
                            res *= i     
                    }     
                    fmt.Println("n! ===", n , res)     
                    // 加锁     
                    lock.Lock()     
                    testMap[n] = res     
                    // 解锁     
                    lock.Unlock() 
            } 
            func main() {     
                    // 使用循环来产生多个协程     
                    for i:=1; i <= 200; i++ {       
                            // 开启200个协程  
                            go jc_test(i)     
                    }     
                    // 为了要执行协程内容 ,需要主线程中等待,不然会直接退出程序     
                    time.Sleep(time.Second * 10)          
                    // 打印出每个元素     
                    lock.Lock()     
                    for i, v := range testMap {         
                            fmt.Printf("testMap[%d]==%d \n", i, v)     
                    }     
                    lock.Unlock()     
                    // 按理说主线程等10秒 上面写的协程都应该执行完毕,后面读数据不应该出现资源竞争问题了     
                    // 但实际运行中还是会出现资源竞争的     
                    // 在读的地方加互斥锁,是因为我们程序设计上可以知道10秒执行完所有协程,但是主线程并不知道     
                    // 因此底层可能任然出现资源争夺,因此加入互斥锁即可解决问题 
            }
            上面的代码有个很大的问题:即我们主线程具体要等多久才合适呢?即引出了管道来实现协程间的通信;
 
 
channel(管道)基本介绍
    上面使用全局变量加锁同步来解决goroutine的通讯,但不是完美的解决方案;通讯机制--channel
    channel的介绍:
        1.channel本质就是一个数据结构--队列
        2.数据是先进先出;[FIFO]
        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  perChan  chan  *Person
        说明:
            1.channel是引用类型
            2.channel必须初始化才能写入数据,即make后才能使用
            3.管道是有类型的,intChan 只能写入整数int;
    
    channel的基本使用:
        // 定义一个管道     
        var intChan chan int      
        intChan = make(chan int, 3)     
        // 看管道是什么?     
        fmt.Println("intChan--", intChan)  // intChan-- 0xc000104080     
        // 向管道写入数据     
        intChan<- 10     
        num := 20     
        intChan<- num      
        // 注意点:写入数据不能超过容量会报deadlock, 因为管道是用来一边写入一边取出的     
        intChan<- 30     
        // intChan<- 40     
        // 看管道的长度和容量(容量是定义时固定的不会动态增加)     
        fmt.Printf("intChan len=%v cap=%v \n", len(intChan), cap(intChan))  // intChan len=2 cap=3     
        // 从管道取出数据     
        var num2 int      
        num2 = <-intChan     
        fmt.Println("num2==",num2)     
        fmt.Printf("get intChan len=%v cap=%v \n", len(intChan), cap(intChan))      
        // 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取也会报deadlock     
        num3 := <-intChan     
        num4 := <-intChan     
        // num5 := <-intChan     
        fmt.Printf("num3==%d  num4==%d \n", num3, num4)
 
        // 当我们想放入任何类型的数据到管道内我们可以使用
        var  allChan  chan  interface{}
        // 但是我们获取时,取出的每个元素就是空接口,要使用类型断言来解决 即:
        cat := <-allChan
        yuan_cat := cat.(Cat)  // 此时的 yuan_cat 才是 Cat结构体,才能取出其中的属性值:yuan_cat.Name
 
    channel的关闭和遍历
        实际案例:
            // channel 的关闭    
            intChan := make(chan int, 10)     
            intChan <- 10     
            intChan <- 20     
            close(intChan)  // close 内置函数是用来专门关闭管道的     
            // intChan <- 30      
            // 关闭管道就不可以再向管道放数据了     
            fmt.Println("ok!!!")     
            // 但是可以获取数据     
            num := <- intChan      
            num2 := <- intChan      
            fmt.Println("num--", num, "num2--", num2)     
            // channel 的遍历 支持 for-range     
            // 在遍历时,如果channel没有关闭,则会出现deadlock的错误     
            // 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历     
            intChan1 := make(chan int , 100)     
            // 写入数据     
            for i := 0; i < cap(intChan1); i++ {         
                    intChan1<- i * 2     
            }     
            close(intChan1)     
            // 取数据     
            for v := range intChan1 {         
                    fmt.Println("v=", v)     
            }
 
    管道阻塞的机制
        如果只是向管道写入数据,而没有读取,就会出现阻塞而deadlock,原因是intChan容量是10,而writeData会写入50个数据,因此会阻塞在writeData的 intChan<- i 
        如果,编译器(运行)发现一个管道只有写,而没有读,则该管道,会阻塞。
        写管道和读管道的频率不一致,无所谓。
    
 
综合案例
    利用goroutine和channel 实现1-200000数字中,哪些是素数(prime)
    代码实现:
        package main 
        import (     "fmt" ) 
        func putNum(intChan chan int) {     
                for i:=1 ; i<= 200000; i ++ {         
                        intChan<- i      
                }      
                close(intChan) 
        } 
        func checkPrime(intChan chan int, primeChan chan int, exitChan chan bool) {     
                for {         
                        num, ok := <-intChan          
                        if !ok {             
                                break          
                        }         
                        flag := true  // 素数标识         
                        for i := 2; i < num; i ++ {             
                                if num % i == 0 {                 
                                        flag = false                  
                                        break              
                                }         
                        }         
                        if flag {             
                                primeChan<- num          
                        }     
            }     
            fmt.Println("有一个primeCheck 管道完成任务, 退出")     
            // 这里不能关闭 primeChan 因为可能别的协程还在写数据     
            // 要写标识退出的 true到 exitChan里     
            exitChan<- true  
        } 
        func main() {     
                // 协程求素数 1-200000数字中,哪些是素数?     
                // 传统for循环方法实现没有问题,但是无法有效利用多核     
                // 将统计素数的任务分配给多个(6)个协程     
                intChan := make(chan int, 10000)     
                primeChan := make(chan int, 100000)     
                exitChan := make(chan bool, 6)     
                go putNum(intChan)     
                // 开启6个协程   统计素数并且放入 primeChan中  
                for i := 0; i < 6; i ++ {         
                        go checkPrime(intChan, primeChan, exitChan)     
                }     
                // 主线程处理     
                go func(){         
                        for i := 0; i < 6; i ++ {             
                                // 不断地从 exitChan 中取值,只有这样主线程才不会马上退出             
                                <-exitChan         
                        }         
                        // 取完说明 所有协程已经把数据全部放入到 primeChan中,然后关闭管道         
                        close(primeChan)     
                }()          
                // 遍历 primeChan 取出结果     
                for {         
                        v, ok := <-primeChan         
                        if !ok {             
                                break          
                        }         
                        fmt.Println("prime num ==", v)     
                }     
                fmt.Println("main 主线程退出~~~") 
            }
 
    传统方式:
        package main 
        import (     "fmt"     "time" ) 
        func main() {     
                start := time.Now().Unix()     
                for num := 1; num <= 200000; num ++ {         
                        flag := true  // 素数标识         
                        for i := 2; i < num; i ++ {             
                                if num % i == 0 {                 
                                        flag = false                  
                                        break              
                                }         
                        }         
                        if flag {              
 
                        }     
                }     
                end := time.Now().Unix()     
                fmt.Println("传统方式耗时--", end - start) 
        }
        
    效率相比,goroutine + channel 比传统方法 快基本是cpu的个数的倍数;
        也可以打开任务管理器看一下开启多个协程时,cpu的使用率(go协程的话开和cpu相同的协程数的话跑的过程中,cpu使用率基本可以达到100%,而传统方式只会提高使用率但是不会达到100%
    
 
 
    
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
    
posted @ 2020-12-31 16:34  以赛亚  阅读(336)  评论(0编辑  收藏  举报