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\n",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\n",intchan,&intchan)

    //3.向管道写入数据
    intchan<- 10
    num := 211
    intchan<- num
    intchan<- 50
    //intchan<-98  //注意点,当我们给管写入数据时,不能超过其容量
    //4.看看管道的长度和cap(容量)
    fmt.Printf("channel 1en=%/cap=%v\n",1en(intchan),cap(intchan))  //3,3

    //5.从管道中读取数据
    var num2 int 
    num2 = <-intchan 
    fmt.Println("num2=",num2)
    fmt.Printf("channel 1en=%v cap=%v\n", 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\n", 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 读到数据=%\n", 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\n",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\n", v)
            time.sleep(time.Second)
        case v := <-stringchan:
            fmt.Printf("从stringchan读取的数据%s\n", v)
            time.sleep(time.Second)
        default:
            fmt.Printf("都取不到了,不玩了,程序员可以加入逻辑\n")
            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)
    }
}

 

posted @ 2020-06-27 17:34  Amorphous  阅读(271)  评论(0编辑  收藏  举报