在這個高併發時代最重要的設計模式無疑是生產者、消費者模式,比如著名的消息隊列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來實現併發式的生產者消費者模式的確是更爲清爽了。