go语言并发编程(channel)

幸运草 2020年4月22日21:54:37前端框架评论阅读模式

一.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("OKn")

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已经被关闭。

特别声明:以上文章内容仅代表作者本人观点,不代表变化吧观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。

  • 赞助本站
  • 微信扫一扫
  • weinxin
  • 加入Q群
  • QQ扫一扫
  • weinxin
幸运草
Go语言接口规则 前端框架

Go语言接口规则

Go语言接口规则 接口是一个或多个方法签名的集合。任何类型的方法集中只要拥有该接口对应的全部方法签名。就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。对应方法,是指有相同名称、参数...
Go语言中处理 HTTP 服务器 前端框架

Go语言中处理 HTTP 服务器

1 概述 包 net/http 提供了HTTP服务器端和客户端的实现。本文说明关于服务器端的部分。 快速开始: package main import (   "log"   "net/http" )...

发表评论