在这个高并发时代最重要的设计模式无疑是生产者、消费者模式,比如著名的消息队列kafka其实就是一个巨型的生产者消费者模式的实现。生产者消费者问题,也称有限缓冲问题,是一个并发环境编程的经典案例。生产者生成一定量的产品放到库房,并不断重复此过程;与此同时,消费者也在缓冲区消耗这些数据,但由于库房大小有限,所以生产者和消费者之间步调协调,生产者不会在库房满的情况放入端口,消费者也不会在库房空时消耗数据。详见下图:
而从GO语言并发模型来看,利用channel确实能达到共享内存的目的,因为channel的性质和一块带有读写状态且保证数据顺序的共享内存并无二致。但通过前面的介绍读者也许也能发现,消息队列的封装程度明显可以做的更高,因此GO语言之父们才说会要通过通信来共享内存。
为了帮助大家找到区别,我们先以Java为例来看一下没有channel的情况下,生产者消费者如何实现。Java的代码及注释如下:
public class Storage {
// 仓库最大存储量
private final int MAX_SIZE = 10;
// 仓库存储的载体
private LinkedList<Object> list = new LinkedList<Object>();
// 锁
private final Lock lock = new ReentrantLock();
// 仓库满的信号量
private final Condition full = lock.newCondition();
// 仓库空的信号量
private final Condition empty = lock.newCondition();
public void produce()
{
// 获得锁
lock.lock();
while (list.size() + 1 > MAX_SIZE) {
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】仓库已满");
try {
full.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】生产一个产品,现库存" + list.size());
empty.signalAll();
lock.unlock();
}
public void consume()
{
// 获得锁
lock.lock();
while (list.size() == 0) {
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】仓库为空");
try {
empty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】消费一个产品,现库存" + list.size());
full.signalAll();
lock.unlock();
}
}
在没有channel的编程语言如JAVA中这种生产者、消费者模式至少要借助一个lock和两个信号量共同完成。其中锁的作用是保证同是时间,仓库中只有一个用户进行数据的修改,而还需要表示仓库满的信号量,一旦达到仓库满的情况则将此信号量置为阻塞状态,从而阻止其它生产者再向仓库运商品了,反之仓库空的信号量也是一样,一旦仓库空了,也要阻其它消费者再前来消费了。
我们刚刚也介绍过了GO语言中的channel其实就是基于lock实现的循环队列,所以不需要再添加lock和信号量就能实现模式了,以下代码中我们通过子goroutine完成了生产者的功能,在主goroutine中实现了消费者的功能,注意channel的读取必须放在不同的goroutine当中,轻而易举的就这完成了生产者消费者模式。下面我们就通过具体实践中来看一下生产者消费者模型的实现。
package main
import (
"fmt"
)
func Product(ch chan<- int) { //生产者
for i := 0; i < 3; i++ {
fmt.Println("Product produceed", i)
ch <- i //由于channel是goroutine安全的,所以此处没有必要必须加锁或者加lock操作.
}
}
func Consumer(ch <-chan int) {
for i := 0; i < 3; i++ {
j := <-ch //由于channel是goroutine安全的,所以此处没有必要必须加锁或者加lock操作.
fmt.Println("Consmuer consumed ", j)
}
}
func main() {
ch := make(chan int)
go Product(ch)
Consumer(ch)
/*运行结果为
Product produceed 0
Product produceed 1
Consmuer consumed 0
Consmuer consumed 1
Product produceed 2
Consmuer consumed 2
*/
}
可以看到和Java比起来使用GO来实现并发式的生产者消费者模式的确是更为清爽了。