go语言并发编程(channel)

Posted 开源你我

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了go语言并发编程(channel)相关的知识,希望对你有一定的参考价值。

一.channel简介


channel是go语言在语言级别提供的goroutine间的通信,我们可以使用channel在两个或者多个协程之间来传递消息。channel是进程内的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如指针等。


channel是类型相关的,也就是说,一个channel只能传递一种类型的值,这个类型需要在声明channel时指定。可以将其认为是一种类型安全的管道。


channel案例:


package main


import "fmt"


func Count(ch chan int) {

    ch <- 1

    fmt.Println("Counting")

}


func main(){

    chs := make([]chan int , 10)

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

        chs[i] = make(chan int)

        go Count( chs[i])

    }


    for _,  ch := range(chs) {

        <-ch

    }

}


二.channel深入


1.channel的基本语法


channel的声明的基本语法形式:


    var chanName chan ElementType


与一般的变量声明不同的地方仅仅是在类型之前加了一个chan关键字,ElementType指的是这个channel所能传递的数据类型。


var ch chan int //这个声明方式只能传递in型数据


或者,咱们声明一个map,元素是bool型的channel


    var m map[string] chan bool //语句合法


定义一个channel也很简单,直接使用内置的make函数


    ch := make(chan int)


在channel用法中,最常见的形式包含读出和读入,将一个数据写入(发送)到channel的语法:

    ch <- value

向channel写入数据通常会导致阻塞,直到有其他的协程从channel中读取数据,从channel中读取数据的语法:


    value := <-ch

当然,如果channel之前没有写入数据的话,那么从channel中读取数据也会导致程序阻塞,直到channel中有数据写入为止。通过一定方式也可以控制channel读和写。


2.Select


早在Unix时代,select机制就已经被引入,通过调用select函数来控制一系列文件句柄,一旦其中一个文件句柄发生了IO动作,该select()调用就会被返回,后来这个机制用来实现多路IO,在Socket中实现并发操作,经常会和其他的一些IO操作做比较,如poll和epoll。go语言也支持在语言级别的select关键字,用于处理异步IO。


select的用法与switch的用法类似,由select开始一个新的选择块,每个选择块由case语句描述。与switch语句可以选择任何使用相等比较的条件相比,select有比较的多的限制,其中最大的一条限制就是每一个case语句里必须是一个IO操作,大致的结构如下:


select (

    case <- chan1:

        //如果有成功读到数据,则进行该case处理语句

    case  chan2<- 1:

        //如果有成功写入数据,则进行该case处理语句

    default:

        //如果上面的操作都没有执行,则执行默认操作

)


可以看得出,select并不像switch,后面并不带判断条件,而是直接去查case语句,每个case语句都必须是一个channel操作,比如上面的例子中,第一个case试图从chan1中读取一个数据并直接忽略读到的数据。而第二个case则写入一个1,如果这两者都没有成功,则执行default。


channel和select结合的简单案例:


ch := make(chan int, 1)

for {

    select {

        case ch <- 0;

        case ch <-1;

    }

    i := <-ch;

    fmt.Println("Value received:", i)

}


完整案例:

package main


import "fmt"func pass(left, right chan int){

    left <- 1 + <- right

}


func main(){

    const n = 50

    leftmost := make(chan int)

    right := leftmost

    left := leftmost    for i := 0; i< n; i++ {

        right = make(chan int)        // the chain is constructed from the end

        go pass(left, right) // the first goroutine holds (leftmost, new chan)

        left = right         // the second and following goroutines hold (last right chan, new chan)    }

    go func(c chan int){ c <- 1}(right)

    fmt.Println("sum:", <- leftmost)

}


这段代码产生了一个单向的管道环,每个节点对输入的值加了1,然后输出给下一个节点,最后到终点 leftmost。重点我认为有以下几个:

1.循环中的 goroutine ;

2.unbuffered channel 的连接和阻塞;

3goroutine 对 channel 的竞争;


3.channel缓冲机制


创建一个带缓冲的channel的语法:c := make(chan int, 1024)


go语言中,channel有缓冲与无缓冲是有重要区别的,那就是一个是同步的;一个是非同步的,如:

c1:=make(chan int)  无缓冲

c2:=make(chan int,1024) 有缓冲

c1<-1      

                      

无缓冲的不仅仅是向c1通道放1而是一直要有别的携程<-c1接手了这个参数,那么c1<-1才会继续下去,要不然就一直阻塞;而c2<-1 则不会阻塞,因为缓冲大小是1024,只有当放第1024值的时候前面的值还没被人拿走,这时候才会阻塞。


有缓冲:


 package main

 import "fmt"

 var c = make(chan int, 1)

 func f() {

     c <- 'c'

     fmt.Println("在goroutine内")

 }


 func main() {

     go f()

     c <- 'c'

     <-c

     <-c

     fmt.Println("外部调用")

 }


无缓冲的案例代码:


package main


import (

    "fmt"

)


func writeRoutine(test_chan chan int, value int) {

    test_chan <- value

}


func readRoutine(test_chan chan int) {

    <-test_chan

    return

}


func main() {

    c := make(chan int)

    x := 100

    //readRoutine(c)

    //go writeRoutine(c, x)

    //writeRoutine(c, x)

    //go readRoutine(c)

    //go readRoutine(c)

    //writeRoutine(c, x)

    go writeRoutine(c, x)

    readRoutine(c)

    fmt.Println(x)

}


4.channel的超时机制

在之前对channel的介绍中,完全没有提到错误的情况,而这个问题显然是不能被忽略的。在并发通信的过程中,最需要处理的就是超时问题,即向channel写数据时发现channel已满,或者channel试图读取数据时发现channel为空,如果不正确出处理这些情况,很可能会导致整个goroutine被锁死。


使用channel时需要小心,比如对于下面的用法:


i := <- ch


不出问题的话一切正常运行,但是如果出现一个错误的情况,即永远都没有人往ch里写数据,那么上述这个读取动作也将永远无法从ch中读到数据,导致整个goroutine永远阻塞并且没有挽留的机会,如果channel只是被同一个开发者使用,那么出问题的可能性要低一些,但是如果一旦对外公开,就必须考虑到最差的情况并且对程序进行保护。


go语言并没有提供直接的超时处理机制,但是我们可以使用select机制,虽然select机制不是专为超时二设置,其实Unix C里面很多时候也使用Select来做超时处理。select能够很方便地解决超时问题。因为select的特点是只要其中的一个case已经完成,程序就会往下执行,而不会考虑其他的case的情况。


示例代码:


timeout := make(chan bool, 1)

go func() {

    time.Sleep(le9)

    timeout <- true

}()


select {

    case <- ch:

        //从ch中读取数据

    case <- timeout:

        //一直没有从ch中读取到数据,但是从timeout中读到了数据

}


这样使用select机制可以避免永久等待问题,因为程序会在timeout中获取到一个数据后继续执行,无论对ch的读取是否处于等待状态,从而达到1秒超时的效果。


超时完整示例代码:


package main


import "time"


func main() {

        ch := make(chan bool)

        end := make(chan bool)

        go func() {

                defer func() { end <- true }()

                select {

                case <-ch:

                        print("OK\n")

                case <-time.After(time.Second * 2):

                }

        }()

        go func() {

                time.Sleep(time.Second * 3)

                ch <- true

        }()

        <-end

}


5.channel的传递


在go语言中channel本身也是一个原生类型,与map之类的地位一样,因为channel本身在定义后也可以通过channel来传递。


可以使用这个特性来实现Uinx上非常常见的管道特性,管道也是使用非常广泛的一种设计模式,比如在处理数据块的时候,我们可以采用管道设计,这样可以比较容易以插件的方式增加数据的处理流程。


使用channel来设计管道的示例代码:


首先定义数据结构:


type PipeData struct {

        value int

        handle func(int) int

        next chan int

}


然后我们写一个常规的处理函数,只要定义一系列的PipeData的数据结构并一起传递给这个函数,就可以达到流式数据处理的目的:


func handle (queue chan *PipeData){

    for data, _ : = range queue {

        data.next <- data.handler(data.value)

    }

}


其实这和其他的编程语言很类似


6.单向channel


我们默认创建的是双向通道,单向通道没有意义,但是我们却可以通过强制转换 

将双向通道 转换成为单向通道 。


var ch1 chan int  // ch1是一个正常的channel,不是单向的  

var ch2 chan<- float64// ch2是单向channel,只用于写float64数据

var ch3 <-chan int // ch3是单向channel,只用于读取int数据 


channel是一个原生类型,因此不仅 支持被传递,还支持类型转换。只有在介绍了单向channel的概念后,读者才会明白类型转换对于 

channel的意义:就是在单向channel和双向channel之间进行转换。


示例如下:

ch4 := make(chan int)

ch5 := <-chan int(ch4) // ch5就是一个单向的读取channel

ch6 := chan<- int(ch4) // ch6 是一个单向的写入channel


基于ch4,我们通过类型转换初始化了两个单向channel:单向读的ch5和单向写的ch6。 

从设计的角度考虑,所有的代码应该都遵循“最小权限原则” , 

从而避免没必要地使用泛滥问题, 进而导致程序失控。 写过C++程序的读者肯定就会联想起const 指针的用法。非const指针具备const指针的所有功能,将一个指针设定为const就是明确告诉 

函数实现者不要试图对该指针进行修改。单向channel也是起到这样的一种契约作用。

下面我们来看一下单向channel的用法:


func Parse(ch <-chan int) {

for value := range ch {

        fmt.Println("Parsing value", value)  

    }

}

除非这个函数的实现者无耻地使用了类型转换,否则这个函数就不会因为各种原因而对ch 进行写,避免在ch中出现非期望的数据,从而很好地实践最小权限原则。


只读只写单向channel代码例子,遵循权限最小化的原则


package main

import "fmt"

import "time"

func sCh(ch <-chan int){

   for val:= range ch {

     fmt.Println(val)

   }

}


func main(){

    dch:=make(chan int,100)

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

      dch<- i  

    }

    go sCh(dch)

    time.Sleep(1e9)

}


7.channel的关闭,以及判断channel的关闭


关闭channel非常简单,直接使用Go语言内置的close()函数即可:close(ch)


在介绍了如何关闭channel之后,我们就多了一个问题:如何判断一个channel是否已经被关闭?我们可以在读取的时候使用多重返回值的方式:x, ok := <-ch

这个用法与map中的按键获取value的过程比较类似,只需要看第二个bool返回值即可,如果返回值是false则表示ch已经被关闭。




以上是关于go语言并发编程(channel)的主要内容,如果未能解决你的问题,请参考以下文章

go语言并发编程(channel)

go 并发编程

GO并发编程基础-channel

go语言之并发编程 channel

GO的并发之道-Goroutine调度原理&Channel详解

Go语言学习之旅--并发编程