web编程

4.4 并发通讯
从上面的例子中能够看到,关键字go的引入使得在Go语言中并发编程变得简单而优雅,但
咱们同时也应该意识到并发编程的原生复杂性,并时刻对并发中容易出现的问题保持警戒。别忘
了,咱们的例子还不能正常工做呢。
事实上,不论是什么平台,什么编程语言,无论在哪,并发都是一个大话题。话题大小一般
也直接对应于问题的大小。并发编程的难度在于协调,而协调就要经过交流。从这个角度看来,

并发单元间的通讯是最大的问题。
在工程上,有两种最多见的并发通讯模型:共享数据和消息。
共享数据是指多个并发单元分别保持对同一个数据的引用,实现对该数据的共享。被共享的
数据可能有多种形式,好比内存数据块、磁盘文件、网络数据等。在实际工程应用中最多见的无
疑是内存了,也就是常说的共享内存。
先看看咱们在C语言中一般是怎么处理线程间数据共享的,如代码清单4-2所示。
在上面的例子中,咱们在10个goroutine中共享了变量counter。每一个goroutine执行完成后,
将counter的值加1。由于10个goroutine是并发执行的,因此咱们还引入了锁,也就是代码中的
lock变量。每次对n的操做,都要先将锁锁住,操做完成后,再将锁打开。在主函数中,使用for
循环来不断检查counter的值(一样须要加锁)。当其值达到10时,说明全部goroutine都执行完
毕了,这时主函数返回,程序退出。
事情好像开始变得糟糕了。实现一个如此简单的功能,却写出如此臃肿并且难以理解的代码。
想象一下,在一个大的系统中具备无数的锁、无数的共享变量、无数的业务逻辑与错误处理分
支,那将是一场噩梦。这噩梦就是众多C/C++开发者正在经历的,其实Java和C#开发者也好不到
哪里去。
Go语言既然以并发编程做为语言的最核心优点,固然不至于将这样的问题用这么无奈的方
式来解决。Go语言提供的是另外一种通讯模型,即以消息机制而非共享内存做为通讯方式。
消息机制认为每一个并发单元是自包含的、独立的个体,而且都有本身的变量,但在不一样并发
单元间这些变量不共享。每一个并发单元的输入和输出只有一种,那就是消息。这有点相似于进程
的概念,每一个进程不会被其余进程打扰,它只作好本身的工做就能够了。不一样进程间靠消息来通
信,它们不会共享内存。
Go语言提供的消息通讯机制被称为channel,接下来咱们将详细介绍channel。如今,让咱们
用Go语言社区的那句著名的口号来结束这一小节:
“不要经过共享内存来通讯,而应该经过通讯来共享内存。”
4.5 channel
channel是Go语言在语言级别提供的goroutine间的通讯方式。咱们可使用channel在两个或
多个goroutine之间传递消息。channel是进程内的通讯方式,所以经过channel传递对象的过程和调
用函数时的参数传递行为比较一致,好比也能够传递指针等。若是须要跨进程通讯,咱们建议用
分布式系统的方法来解决,好比使用Socket或者HTTP等通讯协议。Go语言对于网络方面也有非
常完善的支持。
channel是类型相关的。也就是说,一个channel只能传递一种类型的值,这个类型须要在声
明channel时指定。若是对Unix管道有所了解的话,就不难理解channel,能够将其认为是一种类
型安全的管道。
在了解channel的语法前,咱们先看下用channel的方式重写上面的例子是什么样子的,以此
对channel先有一个直感的认识,如代码清单4-4所示。
在这个例子中,咱们定义了一个包含10个channel的数组(名为chs),并把数组中的每一个
channel分配给10个不一样的goroutine。在每一个goroutine的Add()函数完成后,咱们经过ch <- 1语
句向对应的channel中写入一个数据。在这个channel被读取前,这个操做是阻塞的。在全部的
goroutine启动完成后,咱们经过<-ch语句从10个channel中依次读取数据。在对应的channel写入
数据前,这个操做也是阻塞的。这样,咱们就用channel实现了相似锁的功能,进而保证了全部
goroutine完成后主函数才返回。是否是比共享内存的方式更简单、优雅呢?
咱们在使用Go语言开发时,常常会遇到须要实现条件等待的场景,这也是channel能够发挥
做用的地方。对channel的熟练使用,才能真正理解和掌握Go语言并发编程。下面咱们学习下
channel的基本语法
4.5.1 基本语法
通常channel的声明形式为:
var chanName chan ElementType
与通常的变量声明不一样的地方仅仅是在类型以前加了chan关键字。ElementType指定这个
channel所能传递的元素类型。举个例子,咱们声明一个传递类型为int的channel:
var ch chan int
或者,咱们声明一个map,元素是bool型的channel:
var m map[string] chan bool
上面的语句都是合法的。
定义一个channel也很简单,直接使用内置的函数make()便可:
ch := make(chan int)
这就声明并初始化了一个int型的名为ch的channel。
在channel的用法中,最多见的包括写入和读出。将一个数据写入(发送)至channel的语法
很直观,以下:
ch <- value
向channel写入数据一般会致使程序阻塞,直到有其余goroutine从这个channel中读取数据。从
channel中读取数据的语法是
value := <-ch
若是channel以前没有写入数据,那么从channel中读取数据也会致使程序阻塞,直到channel
中被写入数据为止。咱们以后还会提到如何控制channel只接受写或者只容许读取,即单向
channel。
4.5.2 select
早在Unix时代,select机制就已经被引入。经过调用select()函数来监控一系列的文件句
柄,一旦其中一个文件句柄发生了IO动做,该select()调用就会被返回。后来该机制也被用于
实现高并发的Socket服务器程序。Go语言直接在语言级别支持select关键字,用于处理异步IO
问题。
select的用法与switch语言很是相似,由select开始一个新的选择块,每一个选择条件由
case语句来描述。与switch语句能够选择任何可以使用相等比较的条件相比,select有比较多的
限制,其中最大的一条限制就是每一个case语句里必须是一个IO操做,大体的结构以下:
select {
case <-chan1:
// 若是chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 若是成功向chan2写入数据,则进行该case处理语句
default:
// 若是上面都没有成功,则进入default处理流程
}
能够看出,select不像switch,后面并不带判断条件,而是直接去查看case语句。每一个
case语句都必须是一个面向channel的操做。好比上面的例子中,第一个case试图从chan1读取
一个数据并直接忽略读到的数据,而第二个case则是试图向chan2中写入一个整型数1,若是这
二者都没有成功,则到达default语句。
基于此功能,咱们能够实现一个有趣的程序:
ch := make(chan int, 1)
for {
select {
case ch <- 0:
case ch <- 1:
}
i := <-ch
fmt.Println("Value received:", i)
}
能看明白这段代码的含义吗?其实很简单,这个程序实现了一个随机向ch中写入一个0或者1
的过程。固然,这是个死循环。
4.5.3 缓冲机制
以前咱们示范建立的都是不带缓冲的channel,这种作法对于传递单个数据的场景能够接受,
但对于须要持续传输大量数据的场景就有些不合适了。接下来咱们介绍如何给channel带上缓冲,
从而达到消息队列的效果。
要建立一个带缓冲的channel,其实也很是容易:
c := make(chan int, 1024)
在调用make()时将缓冲区大小做为第二个参数传入便可,好比上面这个例子就建立了一个大小
为1024的int类型channel,即便没有读取方,写入方也能够一直往channel里写入,在缓冲区被
填完以前都不会阻塞。
4.5.4 超时机制
在以前对channel的介绍中,咱们彻底没有提到错误处理的问题,而这个问题显然是不能被忽
略的。在并发编程的通讯过程当中,最须要处理的就是超时问题,即向channel写数据时发现channel
已满,或者从channel试图读取数据时发现channel为空。若是不正确处理这些状况,极可能会导
致整个goroutine锁死。
虽然goroutine是Go语言引入的新概念,但通讯锁死问题已经存在很长时间,在以前的C/C++
开发中也存在。操做系统在提供此类系统级通讯函数时也会考虑入超时场景,所以这些方法一般
都会带一个独立的超时参数。超过设定的时间时,仍然没有处理完任务,则该方法会当即终止并
返回对应的超时信息。超时机制自己虽然也会带来一些问题,好比在运行比较快的机器或者高速
的网络上运行正常的程序,到了慢速的机器或者网络上运行就会出问题,从而出现结果不一致的
现象,但从根本上来讲,解决死锁问题的价值要远大于所带来的问题。
使用channel时须要当心,好比对于如下这个用法:
i := <-ch
不出问题的话一切都正常运行。但若是出现了一个错误状况,即永远都没有人往ch里写数据,那
么上述这个读取动做也将永远没法从ch中读取到数据,致使的结果就是整个goroutine永远阻塞并
没有挽回的机会。若是channel只是被同一个开发者使用,那样出问题的可能性还低一些。但若是
一旦对外公开,就必须考虑到最差的状况并对程序进行保护。
Go语言没有提供直接的超时处理机制,但咱们能够利用select机制。虽然select机制不是
专为超时而设计的,却能很方便地解决超时问题。由于select的特色是只要其中一个case已经
完成,程序就会继续往下执行,而不会考虑其余case的状况。
基于此特性,咱们来为channel实现超时机制:
这样使用select机制能够避免永久等待的问题,由于程序会在timeout中获取到一个数据
后继续执行,不管对ch的读取是否还处于等待状态,从而达成1秒超时的效果。
这种写法看起来是一个小技巧,但倒是在Go语言开发中避免channel通讯超时的最有效方法。
在实际的开发过程当中,这种写法也须要被合理利用起来,从而有效地提升代码质量。
4.5.5 channel的传递
须要注意的是,在Go语言中channel自己也是一个原生类型,与map之类的类型地位同样,因
此channel自己在定义后也能够经过channel来传递。
咱们可使用这个特性来实现*nix上很是常见的管道(pipe)特性。管道也是使用很是普遍
的一种设计模式,好比在处理数据时,咱们能够采用管道设计,这样能够比较容易以插件的方式
增长数据的处理流程。
下面咱们利用channel可被传递的特性来实现咱们的管道。为了简化表达,咱们假设在管道中
传递的数据只是一个整型数,在实际的应用场景中这一般会是一个数据块。
首先限定基本的数据结构:
首先限定基本的数据结构:
type PipeData struct {
value int
handler func(int) int
next chan int
}
而后咱们写一个常规的处理函数。咱们只要定义一系列PipeData的数据结构并一块儿传递给
这个函数,就能够达到流式处理数据的目的:
func handle(queue chan *PipeData) {
for data := range queue {
data.next <- data.handler(data.value)
}
}
这里咱们只给出了大概的样子,限于篇幅再也不展开。同理,利用channel的这个可传递特性,
咱们能够实现很是强大、灵活的系统架构。相比之下,在C++、Java、C#中,要达成这样的效果,
一般就意味着要设计一系列接口。
与Go语言接口的非侵入式相似,channel的这些特性也能够大大下降开发者的心智成本,用
一些比较简单却实用的方式来达成在其余语言中须要使用众多技巧才能达成的效果。













编程

相关文章
相关标签/搜索