zoukankan      html  css  js  c++  java
  • go语言基础(并发--goroutine+channel)

    一、Go协程

    1、Go协程和Go主线程

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

      2)Go协程的特点

        有独立的栈空间

        共享程序堆空间

        调度由用户控制

        协程是轻量级的线程9o线程-协程

    2、案例说明

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

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

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

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

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

    package main
    
    import(
        "fmt"
        "strconv"
        "time"
    )
    
    func test(){
        fori :=1; i<=10; i++{
        fmt.Println("tesst() hello,world"+strconv.rtoa(i))
        time.sleep(time.second)
    }
    
    func main(){
        go test()  ∥开启了一个协程
        for i :=1; i<=10; i++{
            fmt.Print1n("main() he11o,golang"+strconv.rtoa(i))
            time.sleep(time.second)
        }
    }

      程序关系示例图

      

    3、协程小结

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

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

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

    二、goroutine调度模型

    1、MGP模式

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

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

      3)G:协程

    2、模型示例图,简图

      

    三、设置CPU数目

    1、介绍:为了充分了利用多cpu的优势,在Golang程序中,设置运行的cpu数目。

      1)go1.8后,默认让程序运行在多个核上,可以不用设置了

      2)go1.8前,还是要设置一下,可以更高效的利益cpu

    package main
    import "fmt"
    import "runtime"
    
    func main(){
        //获取当前系统cpu的数量
        num := runtime.NumcPU()
    
        //我这里设置num-1的cpu运行go程序
        runtime.GOMAXPROCS(num)
        fmt.Println("num=", num)
    }

    四、不同goroutine之间如何通讯

    1、goroutine之间通讯资源争夺解决方式

      1)全局变量的互斥锁

      2)使用管道channel来解决

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

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

      2)解决方案:加入互斥锁

    package main
    
    import (
        "fmt",
        "runtime",
        "sync"
    )
    
    //需求:现在要计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。
    //最后显示出来。要求使用goroutine完成
    
    //思路
    //1.编写一个函数,来计算各个数的阶乘,并放入到map中.
    //2.我们启动的协程多个,统计的将结果放入到map中
    //3.map应该做出一个全局的。
    
    var(
        myMap = make(map[int] int, 10)
        //声明一个全局的互斥锁
        //1ock 是一个全局的互斥锁,
        //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
        //加锁
        1ock.Lock()
        myMap[n] = res  //concurrent map writes?
        //解锁
        1ock.Unlock()
    }
    
    func main(){
        //我们这里开启多个协程完成这个任务[200个]
        for i := 1; i <= 200; i++{
            go test(i)
        }
    
        //休眠5秒钟【第二个问题】
        time.sleep(time.Second*5)
    
        //这里我们输出结果,变量这个结果
        1ock.Lock()
        for i,v :=range myMap{
            fmt.printf("map[%d]=%d
    ",i,v)
        }
    
        1ock.unlock()
    }

    五、管道(channel)

    1、为什么需要channel

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

      2)主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置10秒,仅仅是估算。

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

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

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

    2、channel的介绍

      1)channle本质就是一个数据结构-列【示意图】

      2)数据是先进先出

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

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

    3、定义/声明channel

      1)channel是引用类型

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

      3)管道是有类型的,intChan只能写入整数int

    var 变量名 chan 数据类型
    
    //举例:
    
    var intChan chan int   //(intChan用于存放int数据)
    
    var mapChan chan map[int]string   //(mapChan用于存放map[int]string类型)
    
    var perChan chan Person  // 结构体Person
    
    var perChan2 chan *Person  // 结构体指针
    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<-98  //注意点,当我们给管写入数据时,不能超过其容量
        //4.看看管道的长度和cap(容量)
        fmt.Printf("channel 1en=%/cap=%v
    ",1en(intchan),cap(intchan))  //3,3
    
        //5.从管道中读取数据
        var num2 int 
        num2 = <-intchan 
        fmt.Println("num2=",num2)
        fmt.Printf("channel 1en=%v cap=%v
    ", 1en(intchan),cap(intchan))  //2,3
    
        //6.在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报错 deadlock
        num3 := <-intChan
        num4 := <-intChan
        num5 := <-intChan 
        fmt.Println("num3=", num3,"num4=", num4,"num5=", num5)
    
    }

    4、代码示例,管道放入map

      创建一个mapChan,最多可以存放10个map[stringlstring的key-val,演示写入和读取。

    func main(){
        var mapchan chan map[string]string 
        mapChan = make(chan map[string]string, 10)
        
        m1 :=make(map[string]string, 20)
        ml["city1"]="北京"
        ml["city2"]="天津"
        
        m2 := make(map[string]string, 20)
        m2["hero1"]="宋江"
        m2["hero2"]="武松”
        
        //..
        mapchan<-m1
        mapChan<-m2
    }

    5、代码示例,管道放入结构体

      创建一个catChan,最多可以存放10个Cat结构体变量,演示写入和读取的用法

    func main(){
        var catchan chan cat 
        catchan = make(chan cat, 10)
        cat1 := cat{Name:"tom", Age:18}
        cat2 := cat{Name:"tom~", Age:19}
        
        catchan<- cat1
        catchan<- cat2
        
        //取出
        cat11 := <-catchan
        cat22 := <-catchan 
        fmt.Println(cat11,cat22)
    }

    6、代码示例,管道放入指针

      创建一个catChan2,最多可以存放10个*Cat变量,演示写入和读取的用法

    func main(){
        var catchan chan *cat 
        catchan=make(chan *cat, 10)
        cat1 := cat{Name:"tom", Age:18}
        cat2 := cat{Name:"tom~", Age:19}
        
        catchan<- &cat1
        catchan<- &kat2
        
        //取出
        cat11 := <-catchan
        cat22 := <-catchan 
        fmt.Printin(cat11, cat22)
    }

    7、代码示例,管道放入任意类型数据

      创建一个allChan,最多可以存放10个任意数据类型变量,演示写入和读取的用法

    func main(){
        var allchan chan interface{}
        allchan = make(chan interface{}, 10)
        cat1:=cat{Name:"tom", Age:18}
        cat2:=cat{Name:"tom~", Age:180}
    
        allchan<- cat1
        allchan<- cat2
        allchan<- 10
        allchan<- "jack"
        
        //取出
        cat11 := <-allchan
        cat22 := <-allchan 
        V1 := <-allchan 
        v2 := <-allchanl 
        fmt.Println(cat11, cat22, v1, v2)
    }

    8、代码示例,管道放入interface处理

      通过管道获取interface类型时,需要进行类型断言

    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
    ", newcat, newcat)
        
        //下面的写法是错误的!编译不通过
        //fmt.Printf("newcat.Name=%v",newcat.Name)
    
        //使用类型断言
        a := newcat.(cat)
        fmt.Printf("newcat.Name=%v", a.Name)
        
    }

    10、channel使用的注意事项

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

      2)channle的数据放满后,就不能再放入了

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

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

    六、channel的遍历和关闭

    1、channel的关闭

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

    2、代码示例,管道关闭

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

    3、channel的遍历

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

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

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

    package main 
    import(
        "fmt"
    )
    
    func main(){
        //遍历管道
        intchan2 := make(chan int, 100)
        for i :=0; i<100; i++{
            intchan2<- i*2  //放入100个数据到管道
        }
    
        //遍历管道不能使用普通的for循环
        //管道长度会改变
        //for i:=e;i< len(intchan2);i++{
        //}
    
        //在遍历时,如果channe1没有关闭,则会出现deadlock的错误
        //在遍历时,如果channe1已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
        Close(intchan2)
    
        for v := range intchan2{
            fmt.Println("v=", v)
        }
    
    }

    4、案例

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

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

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

        3)注意:writeData和readDate操作的是同一个管道

        4)主线程需要等待writeData和readDate协程都完成工作才能退出

    package main 
    import(
        "fmt",
        _"time"
    )
    
    //write Data 
    func writeData(intchan chan int){
        fori :=1; i<=50; i++{
        //放入数据
        intchan<- i 
        fmt.Println("writeData", i)
        time.sleep(time.second)
        c1ose(intchan)  //关闭管道
    }
    
    //read data 
    func readData(intchan chan int, exitchan chan bool){
        for{
            v, ok := <-intchan 
            // 管道为空时退出
            if !ok{
                break 
            }
            fmt.Printf("readpata 读到数据=%
    ", v)
    
            //readData 读取完数据后,即任务完成
            exitchan<- true 
            close(exitchan)
        }
    }
    
    func main(){
        //创建两个管道
        intchan := make(chan int, 50)
        exitchan := make(chan bool, 1)
    
        go writepata(intchan)
        go readpata(intchan, exitchan)
        //time.sleep(time.second*10)
    
        for{
            _, ok := <-exitchan
            if !ok{
                break
            }
        }
    }

    5.案例

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

    package main 
    import(
        "fmt"
    )
    
    //向intchan放入1-8000个数
    func putNum(intchan chan int){
        for i :=1; i<=80; 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{
            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.Print1n("有一个primeNum协程因为取不到数据,退出")
        //这里我们还不能关闭primechan
        //向exitchan写入true 
        exitchan<- true
    }
    
    func main(){
        intchan := make(chan int, 1000)
        primechan := make(chan int, 2000)  //放入的是素数
        //标识退出的管道
        exitchan := make(chan bool, 4)  //4个处理素数的协程标志
    
        //开启一个协程,向intchan放入1-8000个数
        go putNum(intchan)
    
        //开启4个协程,从intchan取出数据,并判断是否为素数,如果是,就放入到primechan
        for i := 1; i<4; i++{
            go primeNum(intchan, primechan, exitchan)
        }
    
        //这里我们主线程,进行处理
        //直接,匿名函数
        go func(){
            for i := 1; i<4; i++{
                <-exitchan
            }
    
            //当我们从exitchan取出了4个结果,就可以放心的关闭primechan 
            close(primechan)
        }()
    
        //遍历我们的primechan,把结果取出
        for{
            res, ok := <-primechan 
            if !ok{
                break
            }
        }
        //将结果输出
        //fmt.Printf("素数=%d
    ",res)
        fmt.Print1n("main线程退出")
    }

    七、channel使用细节和注意事项

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

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

    package main 
    import(
        "fmt"
    )
    
    func main(){
        //管道可以声明为只读或者只写
        //1.在默认情况下下,管道是双向
        var chanl chan int  //可读可写
    
        //2.声明为只写
        var chan2 chan<- int 
        chan2=make(chan int, 3)
        chan2<- 20
        //num:=<-chan2  //error 
        fmt.Printin("chan2=",chan2)
    
        //3.声明为只读
        var chan3 <-chan int 
        num2 := <-chan3
        //chan3<-30  //error
        fmt.Print1n("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方式可以解决
        //1abe1:
        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.s1eep(time.Second)
                return  // 退出for
                //break labe1   //退出到指定label
            }
        }
    
    }

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

    package main 
    import(
        "fmt",
        "time"
    )
    
    //函数
    func sayHel1o(){
        for i := 0; i<10; i++{
        time.sleep(time.second)
        fmt.Println("he11o,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,因为map需要make初始化
    }
    
    func main(){
        go sayHello()
        go test()
    
        for i := 0; i<10; i++{
            fmt.Println("main() ok=", i)
            time.sleep(time.Second)
        }
    }
  • 相关阅读:
    rabbitmq的安装和使用
    springboot之rabbitmq
    springboot之assembly的文件配置
    Android -- 真正的 高仿微信 打开网页的进度条效果
    香蕉云APP,2016下半年开发日记
    使用 Android Studio 检测内存泄漏与解决内存泄漏问题
    阿里云服务器上配置并使用: PHP + Redis + Mysql 从配置到使用
    PHP 获取 特定时间范围 类
    真实记录疑似Linux病毒导致服务器 带宽跑满的解决过程
    -Android -线程池 批量上传图片 -附php接收代码
  • 原文地址:https://www.cnblogs.com/WiseAdministrator/p/13199055.html
Copyright © 2011-2022 走看看