Go 语言 协程和管道讲解

wx592a7561e9493 · · 908 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

参考链接: 角度6-管道

Go 语言 协程和管道讲解 

一、进程和线程基本说明: 

进程是程序在操作系统中一次执行过程,是系统进行资源分配和调度的基本单位;线程是进程的一个执行实例,是程序最小单元,它是比进程更小的能独立运行的基本单位;一个进程可创建和销毁多个线程,同一个进程的多个线程可以并发执行;一个程序至少有一个进程,一个进程至少有一个线程; 

 

 举个栗子: 

 使用的迅雷客户端,打开迅雷就是开启了一个进程,而下载多个视频,就是多个线程在工作; 

 

二、并发、并行简单说明: 

1.并发: 

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

特点: 

  多个任务作用在一个cpu上;从微观的角度看,在一个时间点上,其实只有一个任务在执行,只是时间切片较块;  

2.并行: 

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

特点: 

  多个任务作用在多个cpu上;从微观的角度看,在一个时间点上,多个任务在同时执行;  

并行的速度要快 

三、协程基本介绍: 

1.基本概念: 

一个线程上,可以有多个协程,协程是轻量级的线程; 

协程特点: 

有独立的栈空间;共享程序堆空间;调度由用户控制;协程是轻量级的线程; 

2.快速案例: 

package main


import (

    "fmt"

    "strconv"

    "time"

)


func test(){

    for i:= 0; i < 10; i++{

        fmt.Println("test() " +  strconv.Itoa(i))

        time.Sleep(time.Second)

    }

}


func main() {

    go test()  // 开启一个协程

    for i := 0; i < 10; i++{

        fmt.Println("main()" + strconv.Itoa(i))

        time.Sleep(time.Second)

    }

}

 

主线程是一个物理线程,直接作用在CPU上,非常消耗CPU资源;协程从主线程开启的,是轻量级的线程,对资源消耗小;其它语言的并发机制一般是基于线程,开启过多的线程,资源消耗较大,这就体现出golang的优势; 

3.MPG模式基本介绍: 

M:操作系统的主线程(物理线程);P:协程执行需要的上下文;G:协程; 

MPG模式介绍 

4.设置cpu数: 

package main


import (

    "fmt"

    "runtime"

)


func main () {

    // 查看系统cpu个数

    cpuNum := runtime.NumCPU()

    // 可以自己设置使用多个cpu

    runtime.GOMAXPROCS(cpuNum)

    fmt.Println("cpuNum", cpuNum)

}

 

go 1.8版本以后,默认让程序运行在多核上,可不用设置;go 1.8版本前,需要设置,才可以更高效的利用cpu; 

四、协程之间如何通讯? 

1.全局变量加锁: 

package main


import (

    "fmt"

    "sync"

    "time"

)


var (

    myMap = make(map[int]int, 10)

    // 声明一个全局互斥锁

    // lock 是一个全局互斥锁 sync 是包 同步的意思 Mutex:是互斥

    lock sync.Mutex

)

// test函数计算 n的阶乘, 将结果放到map中

func testCount(n int) {

    res := 1

    for i := 1; i <= n; i++ {

        res *=i

    }

    // 加锁

    lock.Lock()

    myMap[n] = res

    // 解锁

    lock.Unlock()

}

func main () {

    // 开启多个协程完成20个任务

    for i := 1; i <= 20; i++{

        go testCount(i)

    }

    time.Sleep(time.Second * 5)

    lock.Lock()

    for k, v := range myMap{

        fmt.Printf("map[%d]=%d\n", k, v)

    }

    lock.Unlock()

}

 

 

 声明全局互斥锁;写的时候加索,写完释放锁;读的时候加索,读完释放锁; 

 否则会出现资源竞争的问题;报错信息:fatal error: concurrent map writes 

 

全局变量加锁同步是低级程序操作: 

主线程等待所有协程全部完成时间很难确定,因为主线程结束,不管协程是否执行完,程序就此结束;通过全局变量加锁同步实现通讯,也不利于多个协程对全局变量的读写操作; 

2.使用管道channel解决: 

2.1.channel的介绍: 

 声明方式: 

   

   var 变量名 chan 数据类型 

    channel 本质 就是一个数据结构(队列);  数据是先进先出(FIFO);  线程安全,多个协程访问,不需要加锁;  channel只能存放指定数据类型; 

   

   如:一个string的channel只能存放string类型数据 

    channel是引用类型; 

   

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

    channel数据放满后,就不能在放;  channel数据取完后,再取就会报错;  

2.2.快速栗子: 

package main


import "fmt"


func main() {

    // 管道的使用

    // 1.创建一个可以存放3个int类型的管道

    var intChan chan int

    intChan = make(chan int, 3)

    // 2.查看intChan 是什么?

    fmt.Printf("intchan:%v\n", intChan)  // 输出结果: intchan:0xc00008c080 可以看出是引用类型

    // 3.向管道写入数据

    intChan<- 10

    num := 100

    intChan<- num  // 也可以写入常量

    // 4.看看管道的长度和cap(容量:定义的长度跟容量是相等的, 不同于map类型等)

    fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))

    //  4输出结果:channel len=2 cap=3


    //5.从管道中读取数据

    //var num2 int

    num2 := <-intChan

    fmt.Println("取出的num2=", num2)

    fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))

    // 5输出结果:channel len=1 cap=3

}

 

 

 注意: 

 如果往管道中存入数据,管道已经满了,或者取数据,管道中已经没有值,会报错信息fatal error: all goroutines are asleep - deadlock! 

 

2.3.channel关闭: 

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

举个栗子: 

package main


import "fmt"


func main() {

    // 创建一个管道,大小为3

    intChan := make(chan int, 3)

    intChan <- 3

    intChan <- 5

    // 将管道进行关闭

    close(intChan)

    // 此时会无法写入, 因为管道已经关闭: 报错信息 panic: send on closed channel

    //intChan <-6

    n1 := <- intChan

    fmt.Println("可以从管道中读取值:", n1)

}

 

2.4.channel遍历: 

channel 支持 for-range的方式进行遍历: 

在遍历时,若channel没有关闭,出现deadlock错误;在遍历时,若channel已经关闭,会正常遍历数据,遍历完之后,就会退出遍历; 

举个栗子: 

package main


import "fmt"


func main () {

    // 创建一个管道, 大小为200

    intChan := make(chan int, 200)

    for i := 0; i < 200; i++ {

        intChan<- i * 2

    }

    // 在遍历取值时, 一定要关闭管道

    close(intChan)

    // 遍历, 取出管道所有的值

    for value := range intChan{

        fmt.Println("value:", value)

    }

}

 

 

 如果在遍历取值的时候,不关闭管道会报错:fatal error: all goroutines are asleep - deadlock! 

 

2.5.协程与管道的使用: 

package main


import "fmt"


// 往管道里写入50条数据

func writeDate(intChan chan int)  {

    for i := 1; i <= 50; i++{

        // 写入数据

        intChan<- i

        fmt.Println("管道中写入数据:", i)

    }

    // 写完后,关闭此管道

    close(intChan)

}

// 从管道中读取数据

func readData(intChan chan int, exitChan chan bool) {

    for {

        v, ok := <-intChan

        // 说明intChan管道已经取完了

        if !ok{

            break

        }

        fmt.Printf("intChan 管道取出数据:%v\n", v)

    }

    // readData 取完后表示任务已经完成

    exitChan<- true

    close(exitChan)

}

func main()  {

    // 创建两个管道

    intChan := make(chan int, 50)

    // 退出管道, 主线程监控, 协程取完intChan后, 会写进此管道一条数据

    exitChan := make(chan bool, 1)


    // 开启写的协程、读的协程

    go writeDate(intChan)

    go readData(intChan, exitChan)


    // 写一个for循环, 监听exitChan管道, 若exitChan管道的数据取完, 主线程可以结束

    for {

        _, ok := <- exitChan

        if !ok{

            break

        }

    }

}

 

 

 切记,这里创建两个管道,是解决,主线程退出,协程还没有执行完,该程序就结束的问题;如果指向管道写入数据,而没有读取,就会出现阻塞dead lock,原因是超出了管道的容量; 

 

2.6.管道使用细节: 

 声明管道为只写: var chan2 chan<- int

chan2 = make(chan int, 3)

chan2<-20

  声明管道为只读: var chan3 <-chan int

num := <- chan3

  只读或只写,可以应用到函数传参时,做严格校验;  select可以解决从管道取数据阻塞问题: package main


import (

    "fmt"

    "time"

)


func main() {

    // 使用select 可以解决管道数据堵塞的问题

    // 1.定义一个int类型管道, 大小为10

    intChan := make(chan int, 10)

    for i := 0; i < 10; i++{

        intChan<- i

    }

    // 2.定义一个管道 5个数据string

    strChan := make(chan string, 5)

    for i := 0; i < 5; i++ {

        strChan<- "hello" + fmt.Sprintf("%s", i)

    }

    // 传统方法在遍历管到时, 如果不关闭会阻塞 导致deadlock

    // 实际开发中,有时不确定什么时候关闭该管道

    for {

        select {

            // 注意:若intChan 一直没有关闭, 不会一直阻塞而导致deadlock

            // 会自动到下一个case匹配

            case v := <- intChan:

                fmt.Printf("从intChan读取数据%d\n", v)

                time.Sleep(time.Second)

            case v := <- strChan:

                fmt.Printf("从strChan读取数据%s\n", v)

                time.Sleep(time.Second)

            default:

                fmt.Printf("取不到数据咯~~~\n")

                return

        }

    }

}

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


import (

    "fmt"

    "time"

)


func sayGo() {

    // 写一个正常运行的函数

    for i := 0; i < 10; i++{

        time.Sleep(time.Second)

        fmt.Println("hello golang")

    }

}


func testErr() {

    // 使用defer + recover 进行对此函数的异常捕获

    defer func() {

        if err := recover(); err != nil{

            fmt.Printf("test()函数发生错误:%v", err)

        }

    }()


    // 写一个错误的函数

    var myMap map[int]string

    myMap[0] = "hello"

}

func main() {

    go sayGo()

    go testErr() // 若此函数错误, 不会影响到其余函数 所以在此函数里加上捕获异常


    for i := 0; i < 10; i++{

        fmt.Println("main() ok=", i)

        time.Sleep(time.Second)

    }

}



有疑问加站长微信联系(非本文作者)

本文来自:51CTO博客

感谢作者:wx592a7561e9493

查看原文:Go 语言 协程和管道讲解

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

908 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传