Orleans稍微復雜的例子—互動


  這是Orleans系列文章中的一篇.首篇文章在此

  我費力費心的翻譯過官方的教程,但是本人英語詞匯量不高,可是架不住電子詞典啊…只要肯花時間,我這些內容誰都可以做出來.所以這個事例告訴我們一個道理,那就是碼農有三好,錢多話少死得早.我也許只有后兩好.

       當初阿爾法狗在圍棋上戰勝人類的時候,人工智能一時大熱,不管老小,都大肆談論一番深度神經網絡學習算法等等,我當時也是一時興起去拿出我嶄新的書本,說起來都是淚,大學並沒有好好學習,都玩游戲了…不過不管如何,數學系畢業的,底子好點,翻看完必要的知識后,就去看深度神經網絡算法,去了解它們的原理,看到最后,我發現,這不就是一種全分類算法嗎?頓時覺得高大上的深度學習網絡沒有那么神秘了.經常看到這樣的圖片:

 

 

  我們拿過來用,借用微軟的話:一個服務本質上是多個可通信actor的集合,用上面的圖片好像正合適.

       Grain類是輕量級的,為了更好的設計,現實中往往使用grain類來代表顆粒度很細的東西.比如一個商城,可以用GrainA來代表店鋪,用GrainB來代表商品…商鋪管理商品,所以GrainB就會和GrainA之間有互動,在系統中,對每個商品進行單獨控制,就需要很多的GrainB的實例,這在Orleans中很容易做到,更難得的是,可以對GrainB或者GrainA發生的事件進行記錄,實現eventsourcing. Actor的建模方式,使得eventsourcing顯得很自然,也很容易處理.

  讓多個Grain互動起來才有更多的可能性,一個服務本質上是多個可通信actor的集合.所以在這個例子中,我們創造多個Grain實例,讓它們彼此之間有聯系.我們使用”現實中的經理於員工的關系”這樣一個背景模型來構造這個例子.

步驟

我把它們添加到basic項目里,接口如下圖

 

 

還是跟以前一樣,定義接口.這里注意這些接口只有方法,盡量避免使用屬性.雖然現實當中一個經理同時也是一個員工,但是這里的經理接口並沒有繼承自員工接口.這樣做是基於以下考慮:這兩個接口都要被擴展成Grain類,以后對員工和經理的狀態值進行持久化存儲的時候,”非繼承”的兩個grain類會帶來一些方便.稍微說一下持久化:意思就是對Grain類的各個字段進行持久化存儲(存到數據庫或者文件都可以),方便下一次Grain激活的時候讀取各個字段的值.

好了,不管如何,我就是這么勤快的人,直接寫了兩個接口,而不是選擇繼承.現在我勤快的實現這兩個Grain類.

如下圖:

 

 

大部分基礎工作做完了,我們再client調用它們.

 

 

運行之后的截圖如下:

 

 

上個例子雖然正確的運行了,但是隱藏一個小坑,真正正確的操作應該如下圖

 

 

這是因為Grain的方法都是異步的.如果不添加await,在一些耗時的操作中也許會出現先后順序的錯誤.不過對於這個例子,添加不添加與否,並不影響結果.

 

解釋一下

  以前說過,Orleans是基於actor模型構建的,但是Orleans把actor模型做到了極致,它是虛擬的actor模型.一個grain的生命周期,是從一個激活開始,到一個反激活結束.它既然是”激活”而不是構造,這就要求grain類不應該有構造函數,即便是沒有參數的構造函數.一個grain從來不是    從構建到銷毀,而是處於激活或者非激活兩個狀態. 可以在OnActivateAsync()中控制Graini的激活行為,實現類似於”構造”的過程Orleans保證這個方法會在所有其他方法之前調用.在上一個例子中就展現了如何使用這個方法.

  在多線程的編程中,最討厭的兩個事情是死鎖和錯誤處理.有時候,在開發的時候測試一百次都不會死鎖,但是到部署到客戶哪里,一次就給你死鎖.這一般是因為測試的環境簡單化了,沒有考慮到客戶環境的並發量以及並發的時間點.更因為這兩個因素很多時候根本無法考慮.

  消息死鎖

  Orleans編程可以做到全程無鎖,它同時使得多線程編程邏輯上更容易處理,錯誤更容易捕捉.但是這不代表不需要設計.為了展現一下,我再此修改員工和經理.這次我們設計一個問候消息如下:

public class GreetingData
{
    public Guid From { get; set; }
    public string Message { get; set; }
    public int Count { get; set; }
}

 

  我設想,新員工加入的時候經理給一個問候,同時新員工回答thanks,這樣來來回回的消息,就像兩個乒乓球機器人一樣對發對打.加上一個count,本意是想着來回的次數不要超過3次.不然就進入了死循環了.

修改IEmployee 中的Greeting方法的參數.

Task Greeting(GreetingData data);

 

修改對接口的實現.

public async Task Greeting(GreetingData data)
{
    Console.WriteLine("{0} said: {1}", data.From, data.Message);
 
    // stop this from repeating endlessly
    if (data.Count >= 3) return;
 
    // send a message back to the sender
    var fromGrain = GrainFactory.GetGrain<IEmployee>(data.From);
    await fromGrain.Greeting(new GreetingData 
        From = this.GetPrimaryKey(),
        Message = "Thanks!",
        Count = data.Count + 1 });
}

 

 

也更新Manager類,這樣它就會發送一個新的消息.

public async Task AddDirectReport(IEmployee employee)
{
    _reports.Add(employee);
    await employee.SetManager(this);
    await employee.Greeting(new GreetingData {
        From = this.GetPrimaryKey(),
        Message = "Welcome to my team!" });
}

 

現在經理說”Welcome to my team!" ,員工應該回復一個Thanks.

  如果運行這個程序,就造成了一個死鎖,這是因為經理發送消息給員工,並等待回應,員工回答Thanks,並等待經理回應.問題就在於員工的消息永遠得不到回答,因為經理正在忙着等待它第一個消息完成呢,在經理那里,員工的回答必須排隊等待.這就是一個死鎖!!同樣的還有死循環.在Actor編程中死循環往往容易處理(在任何編程中都容易處理),因為它容易定位.但是死鎖就稍微困難點.

  Orleans提供了日志,可以記錄並提示可能的死鎖,它一般用WARNING 體現出來,結尾是About to break its promise. 注意在服務器繁忙的時候,網絡不好的時候,都有可能出現這個警告.

  為了 應對這樣的死鎖,Orleans提供了一個特性[Reentrant],它可以標志一個Grain,使得這個類在處理其他消息的時候,稍微富有”彈性”,可以同時接受其他新消息的到達,(只是”到達”這個步驟”彈性”化,”處理消息”這個步驟依然是剛性的”單線程約束”).使用方法很簡單.如下:

[Reentrant]
public class Employee : Grain, IEmployee
{
    ...
}

 

 處理建議

  Orleans系統分為客戶端和服務端.對於網站來說,Orleans客戶端通常是asp或者其他的http框架,而Orleans服務端通常是數據庫操作端等等.在一個中大型的Orleans系統中,多重Actor互相關聯,最好是自上而下的設計,遵循好消息流或者數據流的流動方向,讓它們的流動可以分支,但是小心處理回流,(需要回流的地方請小心設計,不過這種情況也不會很多,如果很多,請檢查自己的數據設計是否合理).

  在用一個Grain去管理多個Grain類的想法中,還有一點要注意的是,盡量不要產生過於繁忙的Grain實例.由於Grain實例是”單線程機制”的,一個實例操作過於繁忙會影響效率.上例中,如果一個經理直接下屬有一百萬個人.那么這樣設計的程序過熱點就是這個經理.此時需要分散過熱點.一個簡單的辦法是按照員工主標識hash表分段,每一段分給一個經理…此時組合主標識就會派上用場.

  吃瓜群眾應該覺得此處應該有總結:

接口項目就是平時說的”消息協議”.發送消息就是調用接口.在方法內部可以不用鎖而隨意讀寫字段.避免死鎖和過熱的grain實例.

 

 

  上例中設計了單獨的問候類,在.net世界里,在作為參數傳遞和使用的時候,.net是傳遞實例的地址.但是這種機制在並發和多線程的情況下,並不能很好的工作.在Orleans世界里,作為消息的參數(即Grain方法的參數),傳遞的時候默認都是對類實例進行一個深度拷貝,在跨機器的情況下,這是很有必要的.但是如果消息發送的目的地就是在本地silo里,如果還是需要進行深度拷貝,就顯得多余和浪費了.Orleans提供了一個特性[immutable],用它來標明一個類一旦建立就不會再對它進行修改.如果消息目的地是本地silo,使用[immutable]標明的類作為參數時,Orleans就會跳過對此參數的深度拷貝,而是只傳遞一個引用.使用非常簡單.如下:

[Immutable]
public class GreetingData
{
    public Guid From { get; set; }
    public string Message { get; set; }
    public int Count { get; set; }
}

 

       好了,第二個例子就說完了.有點Orleans實際項目的影子了,但是還是不夠…

       似乎應該更進一步,那就是存儲,把Grain的狀態值保存起來,下一次激活的時候,恢復它的狀態值,這樣才能符合現實需要.可以嘗試在OnActivateAsync里控制讀取動作,在OnDeactivateAsync中控制存儲動作.不過微軟提供了更為通用的存儲中間件(StorageProvider),使得這些工作變得容易.所以完全沒有必要自己另外實現一個(當然你願意也行).要想使用這些存儲中間件,就必須配置一下.可以使用配置文件,也可以在代碼里配置.所以持久化就按下暫且不表,我來說說配置.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM