三分鐘掌控Actor模型和CSP模型

部落格猿馬甲哥發表於2022-03-26

回顧一下前文《三分鐘掌握共享記憶體模型和 Actor模型》

Actor vs CSP模型

  • 傳統多執行緒的的共享記憶體(ShareMemory)模型使用lock,condition等同步原語來強行規定程式的執行順序。
  • Actor模型,是基於訊息傳遞的併發模型,強調的是Actor這個工作實體,每個Actor自行決定訊息傳遞的方向(要傳遞的ActorB),通過訊息傳遞形成流水線。

本文現在要記錄的是另一種基於訊息傳遞的併發模型: CSP(communicating sequential process順序通訊過程)。

在CSP模型,worker之間不直接彼此聯絡,強調通道在訊息傳遞中的作用,不謀求形成流水線。

訊息的傳送者和接受者通過該通道鬆耦合,傳送者不知道自己訊息被哪個接受者消費了,接受者也不知道是從哪個傳送者傳送的訊息。

go的通道

go的通道是golang協程同步和通訊的原生方式。

同map,slice一樣,channel通過make內建函式初始化並返回引用,引用可認為是常量指標

兩種通道:

  1. 無緩衝區通道:讀寫兩端就緒後,才能通訊(一方沒就緒就阻塞)

這種方式可以用來在goroutine中進行同步,而不必顯式鎖或者條件變數。

  1. 有緩衝區通道:就有可能不阻塞, 只有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個自動重置事件來切換通知,由一個執行緒通知另外一個執行緒執行。

相關文章