多核处理器越来越普及。有没有一种简单的办法,能够让我们写的软件释放多核的威力?是有的。随着Golang,Erlang,Scala等为并发设计的程序语言的兴起,新的并发模式逐渐清晰。正如过程式编程和面向对象一样,一个好的编程模式有一个极其简洁的内核,还有在此之上丰富的外延。可以解决现实世界中各种各样的问题。本文以GO语言为例,解释其中内核、外延。
并发模式之内核
这种并发模式的内核只需要协程和通道就够了。协程负责执行代码,通道负责在协程之间传递事件。
不久前,并发编程是个非常困难的事。要想编写一个良好的并发程序,我们不得不了解线程,锁,semaphore,barrier甚至cpu更新高速缓存的方式,而且他们个个都有怪脾气,处处是陷阱。笔者除非万不得以,决不会自己操作这些底层并发元素。一个简洁的并发模式不需要这些复杂的底层元素,协程和通道就够了。
协程是轻量级的线程。在过程式编程中,当调用一个过程的时候,需要等待其执行完才返回。而调用一个协程的时候,不需要等待其执行完,会立即返回。协程十分轻量,Go语言可以在一个进程中执行有数以十万计的协程,依旧保持高性能。而对于普通的平台,一个进程有数千个线程,其cpu会忙于上下文切换,性能急剧下降。随意创建线程可不是一个好主意,但是我们可以大量使用的协程。
通道是协程之间的数据传输通道。通道可以在众多的协程之间传递数据,具体可以值也可以是个引用。通道有两种使用方式。
- 协程可以试图向通道放入数据,如果通道满了,会挂起协程,直到通道可以为他放入数据为止。
- 协程可以试图向通道索取数据,如果通道没有数据,会挂起协程,直到通道返回数据为止。
如此,通道就可以在传递数据的同时,控制协程的运行。有点像事件驱动,也有点像阻塞队列。
这两个概念非常的简单,各个语言平台都会有相应的实现。在Java和C上也各有库可以实现两者。
Golang | Erlang | Scala(Actor) | |
协程 | goroutines | process | actor |
消息队列 | channel | mailBox | channel |
只要有协程和通道,就可以优雅的解决并发的问题。不必使用其他和并发有关的概念。那如何用这两把利刃解决各式各样的实际问题呢?
并发模式之外延
协程相较于线程,可以大量创建。打开这扇门,我们拓展出新的用法,可以做生成器,可以让函数返回“服务”,可以让循环并发执行,还能共享变量。但是出现新的用法的同时,也带来了新的棘手问题,协程也会泄漏,不恰当的使用会影响性能。下面会逐一介绍各种用法和问题。演示的代码用GO语言写成,因为其简洁明了,而且支持全部功能。
生成器
有的时候,我们需要有一个函数能不断生成数据。比方说这个函数可以读文件,读网络,生成自增长序列,生成随机数。这些行为的特点就是,函数的已知一些变量,如文件路径。然后不断调用,返回新的数据。
下面生成随机数为例, 以让我们做一个会并发执行的随机数生成器。
非并发的做法是这样的:
func rand_generator_1() int { return rand.Int() }
上面是一个函数,返回一个int。假如rand.Int()这个函数调用需要很长时间等待,那该函数的调用者也会因此而挂起。所以我们可以创建一个协程,专门执行rand.Int()。
// 函数 rand_generator_2,返回 通道(Channel) func rand_generator_2() chan int { // 创建通道 out := make(chan int) // 创建协程 go func() { for { //向通道内写入数据,如果无人读取会等待 out <- rand.Int() } }() return out } func main() { // 生成随机数作为一个服务 rand_service_handler := rand_generator_2() // 从服务中读取随机数并打印 fmt.Printf("%d\n",<-rand_service_handler) }