美文网首页Go语言实践Go
Go语言学习笔记19.并发编程

Go语言学习笔记19.并发编程

作者: 快乐的提千万 | 来源:发表于2019-11-07 10:46 被阅读0次

    前置知识点:(如果不了解建议百度下再学并发编程)

    • 并发、并行,同时进行叫并行,交替进行叫并发(时间片轮循)。
    • 进程、线程、协程
    • 进程的状态,挂起、休眠、运行等等
    • 进程的关系,比如子进程、子线程、孤儿进程、僵尸进程,守护进程等等。

    协程 goroutine

    从创建的消耗来比较,进程>线程>协程。
    有很多时候我们只是想多个座位,结果却不得不去盖一栋房子。而协程就是这个凳子,进程就是房子。而进程和线程的消耗差距比这个要大几千倍。

    创建

    go func(){
    }
    
    package main
    
    import (
        "fmt"
        "time"
    )
    
    func newTask() {
        index := 1
        fmt.Println("协程创建了")
        for {
            time.Sleep(time.Second * 2) //延时2s
            fmt.Println("协程 index",index)
            index ++
        }
    }
    
    func main() {
    
        go newTask() //新建一个协程, 新建一个任务
        index := 1
        //死循环
        for {
            time.Sleep(time.Second) //延时1s
            fmt.Println("主进程 index",index)
            index ++
        }
    }
    

    输出:

    协程创建了
    主进程 index 1
    协程 index 1
    主进程 index 2
    主进程 index 3
    协程 index 2
    主进程 index 4
    主进程 index 5
    协程 index 3
    主进程 index 6
    主进程 index 7
    协程 index 4
    主进程 index 8
    ...
    

    和创建子进程或子线程是一样的逻辑,互不干扰。
    当主进程或主协程停止的时候,子协程也会停止。

    相关函数

    1. runtime.Gosched() 让出时间片
        go func() {
            for i := 0; i < 5; i++ {
                fmt.Println("go")
            }
        }()
    
        for i := 0; i < 2; i++ {
            //让出时间片,先让别的协议执行,它执行完,再回来执行此协程
            runtime.Gosched()
            fmt.Println("hello")
        }
    
    1. runtime.Goexit() 终止当前 goroutine 执⾏,调度器确保所有已注册 defer延迟调用被执行。
    package main
    
    import (
        "fmt"
        "runtime"
    )
    
    func test() {
        defer fmt.Println("协程结束")
    
        //return //终止此函数
        runtime.Goexit() //终止所在的协程
    
        fmt.Println("这句话不会被打印")
    }
    
    func main() {
    
        //创建新建的协程
        go func() {
            fmt.Println("协程开始前")
    
            //再创建一个
            test()
    
            runtime.Gosched()
            fmt.Println("协程创建了")
        }() //别忘了()
        
        //死循环
        for  {}
    }
    
    1. runtime.GOMAXPROCS() 设置可以并行计算的CPU核数的最大值,并返回之前的值。
      比如有两个协程,设置1,就会交替执行,如果设置2,就会一起执行。
        n := runtime.GOMAXPROCS(2) //指定以1核运算
        //n := runtime.GOMAXPROCS(4) //指定以4核运算
        fmt.Println("n = ", n)
    
        for i:=1;i<100000;i++{
            go fmt.Print(1)
            fmt.Print(0)
        }
    

    从打印看效果不是很明显,但是可以用耗时任务来看。

    package main
    
    import (
        "fmt"
        "runtime"
        "time"
    )
    
    func main() {
        n := runtime.GOMAXPROCS(2)
        fmt.Println("n = ", n)
    
        go func(){
            for { //抢光所有资源
            }   
        }()
        for i:=1;i<100000;i++{
            fmt.Print(0)
            time.Sleep(time.Second)
        }   
    }
    

    这个就很明显了,如果只用1核,最多打印一个0,然后sleep,资源就全部被协程抢了,再也打印不出来了。但是如果用2核,则可以打印出来。
    有人可能会说,不是说交替执行么,但是这里面的协程是死循环,事情一直没有完成,而主进程的sleep会表示我还可以等,则CPU不会让出时间片。
    如果都是死循环会怎么样?会交替执行,因为主进程说,我也有急事,CPU则会让他们各自执行一段时间。

    channel

    之前的进程、线程之间,会通过共享内存来通信,但是数据都是存放在各自的家里。但是协程则是通过通信来共享内存。
    channel本意是管道,控制并发最好的方法是什么?排队啊!管道就是将所有人的任务按顺序执行。

    创建:
    make(chan Type, capacity) capacity表示缓存区大小
    关闭:
    close(chan Type)

    package main
    
    import (
        "fmt"
        "time"
    )
    
    //定义一个打印机,参数为字符串,按每个字符打印
    //打印机属于公共资源
    func Printer(str string) {
        for _, data := range str {
            fmt.Printf("%c", data)
            time.Sleep(time.Second)
        }
        fmt.Printf("\n")
    }
    
    func person1() {
        Printer("hello")
    }
    
    func person2() {
        Printer("world")
    }
    
    //全局变量,创建一个channel
    var ch = make(chan int)
    
    //person3执行完后,才能到person4执行
    func person3() {
        Printer("HELLO")
        ch <- 666 //给管道写数据,发送
        close(ch)
    }
    
    func person4() {
        //从管道取数据,接收,如果通道没有数据他就会阻塞,所有前面的会先执行
        if num, ok := <-ch; ok == true {
            fmt.Println("num = ", num)
        } else { //管道关闭
            fmt.Println("管道关闭了")
        }
        Printer("WORLD")
    }
    
    func main() {
        //新建2个协程,代表2个人,2个人同时使用打印机
        //没有管道的情况,会抢  输出:hwoelrllod
        //go person1()
        //go person2()
    
        //有管道的情况,顺序执行: 
        /*
        HELLO
        num =  666
        WORLD
        */
        go person3()
        go person4()
        
        //特地不让主协程结束,死循环
        for {
    
        }
    }
    

    管道的缓冲
    无缓冲,则必须要发送方和接收方同时在才可以继续,否则发送方发了一个后就会阻塞。
    有缓存,在缓冲大小内,发送方可以一直发,直到缓冲区满,接收方什么时候来收都可以。

        //创建一个无缓存的channel
        ch := make(chan int, 0)
        //创建一个有缓存的channel
        //ch := make(chan int, 3)
    
        //len(ch)缓冲区剩余数据个数, cap(ch)缓冲区大小
        fmt.Printf("len(ch) = %d, cap(ch)= %d\n", len(ch), cap(ch))
    
        //新建协程
        go func() {
            for i := 0; i < 3; i++ {
                fmt.Printf("子协程:i = %d\n", i)
                ch <- i //往chan写内容
            }
        }()
    
        //延时,没有缓存则发送方也会等待
        time.Sleep(2 * time.Second)
    
        for i := 0; i < 3; i++ {
            num := <-ch //读管道中内容,没有内容前,阻塞
            fmt.Println("num = ", num)
        }
    

    管道的方向限定

        //创建一个channel, 双向的
        ch := make(chan int)
    
        //双向channel能隐式转换为单向channel
        var writeCh chan<- int = ch //只能写,不能读
        var readCh <-chan int = ch  //只能读,不能写
        
        writeCh <- 666 //写
        <-readCh //读
    

    相关文章

      网友评论

        本文标题:Go语言学习笔记19.并发编程

        本文链接:https://www.haomeiwen.com/subject/lskebctx.html