回顧一下前文《三分鐘掌握共享記憶體模型和 Actor模型》
Actor vs CSP模型
- 傳統多執行緒的的共享記憶體(ShareMemory)模型使用lock,condition等同步原語來強行規定程式的執行順序。
- Actor模型,是基於訊息傳遞的併發模型,強調的是Actor這個工作實體,每個Actor自行決定訊息傳遞的方向(要傳遞的ActorB),通過訊息傳遞形成流水線。
本文現在要記錄的是另一種基於訊息傳遞的併發模型: CSP(communicating sequential process順序通訊過程)。
在CSP模型,worker之間不直接彼此聯絡,強調通道在訊息傳遞中的作用,不謀求形成流水線。
訊息的傳送者和接受者通過該通道鬆耦合,傳送者不知道自己訊息被哪個接受者消費了,接受者也不知道是從哪個傳送者傳送的訊息。
go的通道
go的通道是golang協程同步和通訊的原生方式。
同map,slice一樣,channel通過make內建函式初始化並返回引用,引用可認為是常量指標。
兩種通道:
- 無緩衝區通道:讀寫兩端就緒後,才能通訊(一方沒就緒就阻塞)
這種方式可以用來在goroutine中進行同步,而不必顯式鎖或者條件變數。
- 有緩衝區通道:就有可能不阻塞, 只有buffer滿了,寫入才會阻塞;只有buffer空了,讀才會阻塞。
go的通道暫時先聊到這裡。
我們來用以上背景做一道 有意思的面試題吧 。
兩個執行緒輪流列印0到100?
我不會啥演算法,思路比較弱智:#兩執行緒#, #列印奇/偶數#, 我先復刻這兩個標籤。
通過go的無緩衝通道的同步阻塞的能力對齊每一次迴圈。
package main
import (
"fmt"
"strconv"
"sync"
)
var wg sync.WaitGroup
var ch1 = make(chan struct{})
func main() {
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i <= 100; i++ {
ch1 <- struct{}{}
if i%2 == 0 { // 偶數
fmt.Println("g0 " + strconv.Itoa(i))
}
}
}()
go func() {
defer wg.Done()
for i := 0; i <= 100; i++ {
<-ch1
if i%2 == 1 { // 奇數
fmt.Println("g1 " + strconv.Itoa(i))
}
}
}()
wg.Wait()
}
題解: 兩個協程都執行0到100次迴圈,但是不管哪個執行緒跑的快,在每次迴圈輸出時均會同步對齊, 每次迴圈時只輸出一個奇/偶值, 這樣也不用考慮兩個協程的啟動順序。
我們來思考我的老牌勁語C#要完成本題要怎麼做?
依舊是#兩執行緒#、#列印奇偶數#。
volatile static int i = 0;
static AutoResetEvent are = new AutoResetEvent(true);
static AutoResetEvent are2 = new AutoResetEvent(false);
public static void Main(String[] args)
{
Thread thread1 = new Thread(() =>
{
for (var i=0;i<=100;i++)
{
are.WaitOne();
if (i % 2 == 0)
{
Console.WriteLine(i + "== 偶數");
}
are2.Set();
}
});
Thread thread2 = new Thread(() =>
{
for (var i = 0; i <= 100; i++)
{
are2.WaitOne();
if (i % 2 == 1)
{
Console.WriteLine(i + "== 奇數");
}
are.Set();
}
});
thread1.Start();
thread2.Start();
Console.ReadKey();
}
注意兩個:
- volatile:提醒編譯器或執行時不對欄位做優化(處於效能,編譯器/runtime會對同時執行的執行緒訪問的同一欄位進行優化,加volatile忽略這種優化 )。
- Object-->MarshalByRefObject-->WaitHandle-->EventWaitHandle--->AutoResetEvent
本次使用了2個自動重置事件來切換通知,由一個執行緒通知另外一個執行緒執行。