ET筆記_協(xié)成_異步

2.1 什么是協(xié)成

(黑色字體為熊貓腾务,貓大原話。紅色自己理解)

說到協(xié)成削饵,我們先了解什么是異步岩瘦,異步簡單來說就是,我要發(fā)起一個(gè)調(diào)用窿撬,但是這個(gè)被調(diào)用方(可能是其他線程启昧,也可能是IO)出結(jié)果需要一段時(shí)間,我不想這個(gè)調(diào)用阻塞竹調(diào)用方的整個(gè)線程劈伴,因此傳給被調(diào)用方一個(gè)回調(diào)函數(shù)密末,被調(diào)用方運(yùn)行完成后回調(diào)這個(gè)回調(diào)函數(shù)就能通知調(diào)用方繼續(xù)往下執(zhí)行。

static void Main(string[] args)

? ? ? ? {

? ? ? ? ? ? int loopCount = 0;

? ? ? ? ? ? while (true)

? ? ? ? ? ? {

? ? ? ? ? ? ? ? Thread.Sleep(1);

? ? ? ? ? ? ? ? ++loopCount;

? ? ? ? ? ? ? ? if (loopCount % 10000==0)

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? Console.WriteLine($"loop count:{loopCount}");

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

這時(shí)我需要加個(gè)功能,在程序一開始严里,我希望在5秒鐘之后打印出loopCount的值新啼。看到5秒后我們可以想到Sleep方法刹碾,它會(huì)阻塞線程一定時(shí)間然后繼續(xù)執(zhí)行燥撞。我們顯然不能在主線程中Sleep,因?yàn)闀?huì)破壞掉每10000次計(jì)數(shù)打印一次的邏輯迷帜。

private static int loopCount = 0;

? ? ? ? static void Main(string[] args)

? ? ? ? {

? ? ? ? ? ? OneThreadSynchronizationContext _ = OneThreadSynchronizationContext.Instance;

? ? ? ? ? ? WaitTimeAsync(5000,WaitTimeFinishCallback);

? ? ? ? ? ? while (true)

? ? ? ? ? ? {

? ? ? ? ? ? ? ? OneThreadSynchronizationContext.Instance.Update();

? ? ? ? ? ? ? ? Thread.Sleep(1);

? ? ? ? ? ? ? ? ++loopCount;

? ? ? ? ? ? ? ? if (loopCount % 10000==0)

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? Console.WriteLine($"loop count:{loopCount}");

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? /// <summary>

? ? ? ? /// 回調(diào)函數(shù)

? ? ? ? /// </summary>

? ? ? ? private static void WaitTimeFinishCallback()

? ? ? ? {

? ? ? ? ? ? Console.WriteLine($"WaitTimeAsync finish loopCount的值是:{loopCount}");

? ? ? ? }

? ? ? ? private static void WaitTimeAsync(int waitTime,Action action)

? ? ? ? {

? ? ? ? ? ? Thread thread = new Thread(()=> WaitTime(waitTime,action));

? ? ? ? ? ? thread.Start();

? ? ? ? }

? ? ? ? /// <summary>

? ? ? ? /// 在另外的線程等待

? ? ? ? /// </summary>

? ? ? ? private static void WaitTime(int waitTime,Action action)

? ? ? ? {

? ? ? ? ? ? Thread.Sleep(waitTime);

? ? ? ? ? ? // 將action扔回主線程

? ? ? ? ? ? OneThreadSynchronizationContext.Instance.Post(o=>action(),null);

? ? ? ? }

我們這里設(shè)計(jì)了一個(gè)WaitTimeAsync方法物舒,WaitTimeAsync其實(shí)就是一個(gè)典型的異步方法,它從竹線程發(fā)起調(diào)用瞬矩,傳入了一個(gè)WaitTimeFinishCallback回調(diào)方法做參數(shù)茶鉴,開啟了一個(gè)線程,線程Sleep一定時(shí)間后景用,將傳過來的回調(diào)扔回到竹線程程序執(zhí)行涵叮。OneThreadSynchronizationContext是一個(gè)跨線程隊(duì)列,任何線程可以往里面扔委托伞插。OneThreadSynchronizationContext的Update方法在主線程中調(diào)用割粮,會(huì)將這些委托取出來放在主線程執(zhí)行,為什么回調(diào)方法需要扔回到主線程執(zhí)行呢媚污?隱晦回調(diào)方法中讀取了loopCount,loopCount在主線程中也有讀寫舀瓢,所以要么加鎖,要么永遠(yuǎn)保證只在主線程中讀取耗美。加鎖是個(gè)不好的做法京髓,代碼中到處都是鎖會(huì)導(dǎo)致閱讀跟維護(hù)困難,很容易產(chǎn)生多線程bug商架。這中將邏輯打包成委托然后扔回另外一個(gè)線程多線程開發(fā)中常用的技巧堰怨。

我們可能又需要改動(dòng)需求,WaitTimeFinishCallback執(zhí)行完成之后蛇摸,再想等3秒备图,再打印一下loopCount.

? ? ? /// <summary>

? ? ? ? /// 回調(diào)函數(shù)

? ? ? ? /// </summary>

? ? ? ? private static void WaitTimeFinishCallback()

? ? ? ? {

? ? ? ? ? ? Console.WriteLine($"WaitTimeAsync finish loopCount的值是:{loopCount}");

? ? ? ? ? ? WaitTimeAsync(3000, WaitTimeFinishCallback2);

? ? ? ? }

? ? ? ? private static void WaitTimeFinishCallback2()

? ? ? ? {

? ? ? ? ? ? Console.WriteLine($"WaitTimeAsync finish loopCount的值是:{loopCount}");

? ? ? ? }

? ? ? ? private static void WaitTimeAsync(int waitTime,Action action)

? ? ? ? {

? ? ? ? ? ? Thread thread = new Thread(()=> WaitTime(waitTime,action));

? ? ? ? ? ? thread.Start();

? ? ? ? }

我們這是可能仍然需要改需求,三秒后繼續(xù)赶袄,接下來四秒繼續(xù)打印揽涮。這樣的話如同上面我們還需要寫個(gè)回調(diào)函數(shù),在三秒結(jié)束后調(diào)用饿肺。這樣插入代碼蒋困,顯得非常繁瑣。這里可以回答什么是協(xié)成唬格,\color{green}{這樣一串串回調(diào)就是協(xié)程}家破。

\color{red}{OneThreadSynchroniationContext}:

OneThreadSynchronizationContext類颜说,他繼承了SynchronizationContext類,而SyncehronizationContext提供在各種同步模型中傳播同步上下文的基礎(chǔ)公共汰聋。

\color{red}{OneThreadSynchronizationContext:收集各個(gè)線程的回調(diào)方法门粪,并且放回到主線程進(jìn)行}

public class OneThreadSynchronizationContext:SynchronizationContext

? ? {

? ? ? ? /// <summary>

? ? ? ? /// 單列

? ? ? ? /// </summary>

? ? ? ? public static OneThreadSynchronizationContext Instance { get; } = new OneThreadSynchronizationContext();

? ? ? ? /// <summary>

? ? ? ? /// 線程Id

? ? ? ? /// </summary>

? ? ? ? private readonly int mainThreadid = Thread.CurrentThread.ManagedThreadId;

? ? ? ? /// <summary>

? ? ? ? ///? 線程同步隊(duì)列 (回調(diào)的方法)

? ? ? ? /// </summary>

? ? ? ? private readonly ConcurrentQueue<Action> queue = new ConcurrentQueue<Action>();

? ? ? ? /// <summary>

? ? ? ? /// 委托

? ? ? ? /// </summary>

? ? ? ? private Action a;

? ? ? ? public void Update()

? ? ? ? {

? ? ? ? ? ? while (true)

? ? ? ? ? ? {

? ? ? ? ? ? ? ? // 出隊(duì)成功烹困,執(zhí)行委托玄妈。否則結(jié)束這次判斷

? ? ? ? ? ? ? ? if (!this.queue.TryDequeue(out a))

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? return;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? a();

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? /// <summary>

? ? ? ? /// 回調(diào)方法如隊(duì)

? ? ? ? /// </summary>

? ? ? ? /// <param name="callback"></param>

? ? ? ? /// <param name="state"></param>

? ? ? ? public override void Post(SendOrPostCallback callback, object state)

? ? ? ? {

? ? ? ? ? ? // 當(dāng)前線程是主線程這直接執(zhí)行回調(diào)函數(shù),否則加入回調(diào)隊(duì)列

? ? ? ? ? ? if (Thread.CurrentThread.ManagedThreadId==this.mainThreadid)

? ? ? ? ? ? {

? ? ? ? ? ? ? ? callback(state);

? ? ? ? ? ? ? ? return;

? ? ? ? ? ? }

? ? ? ? ? ? this.queue.Enqueue(() => { callback(state); });

? ? ? ? }

? ? }

更好的協(xié)成:異步

上文講了一串回調(diào)就是協(xié)成髓梅,顯然這樣寫代碼拟蜻,增加邏輯,插入邏輯非常容易出錯(cuò)枯饿。我們需要利用異步語法把這個(gè)異步回調(diào)的形式改成同步的像是酝锅,幸好C#已經(jīng)幫我們?cè)O(shè)計(jì)好了。

? ? ? private static int loopCount = 0;

? ? ? ? static void Main(string[] args)

? ? ? ? {

? ? ? ? ? ? OneThreadSynchronizationContext _ = OneThreadSynchronizationContext.Instance;

? ? ? ? ? ? //WaitTimeAsync(5000,WaitTimeFinishCallback);

? ? ? ? ? ? Console.WriteLine($"主線程:{Thread.CurrentThread.ManagedThreadId}");

? ? ? ? ? ? Crontine();

? ? ? ? ? ? while (true)

? ? ? ? ? ? {

? ? ? ? ? ? ? ? OneThreadSynchronizationContext.Instance.Update();

? ? ? ? ? ? ? ? Thread.Sleep(1);

? ? ? ? ? ? ? ? ++loopCount;

? ? ? ? ? ? ? ? if (loopCount % 10000==0)

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? Console.WriteLine($"loop count:{loopCount}");

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? private static async void Crontine()

? ? ? ? {

? ? ? ? ? ? await WatiTimeAsync(5000);

? ? ? ? ? ? Console.WriteLine($"1當(dāng)前線程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");

? ? ? ? ? ? await WatiTimeAsync(4000);

? ? ? ? ? ? Console.WriteLine($"2當(dāng)前線程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");

? ? ? ? ? ? await WatiTimeAsync(3000);

? ? ? ? ? ? Console.WriteLine($"3當(dāng)前線程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");

? ? ? ? }

? ? ? ? private static Task WatiTimeAsync(int waitTime)

? ? ? ? {

? ? ? ? ? ? TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

? ? ? ? ? ? Thread thread = new Thread(()=> WaitTime(waitTime,tcs)); // 開啟一個(gè)線程

? ? ? ? ? ? thread.Start();

? ? ? ? ? ? return tcs.Task;

? ? ? ? }

? ? ? ? /// <summary>

? ? ? ? /// 另外一個(gè)線程操作

? ? ? ? /// </summary>

? ? ? ? /// <param name="waitTime"></param>

? ? ? ? /// <param name="tcs"></param>

? ? ? ? private static void WaitTime(int waitTime,TaskCompletionSource<bool> tcs)

? ? ? ? {

? ? ? ? ? ? Thread.Sleep(waitTime);

? ? ? ? ? ? // 將tcs扔回主線程

? ? ? ? ? ? OneThreadSynchronizationContext.Instance.Post(o=>tcs.SetResult(true),null);

? ? ? ? }

在這段代碼里奢方,WaitTimeAsync方法中搔扁,我們利用了TaskCompletionSource類替代了之前傳入的Action參數(shù),WaitTimeAsync方法反悔了一個(gè)Task類型的結(jié)果蟋字。WaitTime中我們把a(bǔ)ction()替換成了tcs.SetResult(true),WaitTimeAsync方法錢使用await關(guān)鍵字稿蹲,這樣可以將一連串的回調(diào)改成同步形式(\color{red}{await阻塞,這樣就會(huì)從上往下執(zhí)行鹊奖,這樣開起來和我們平時(shí)開發(fā)執(zhí)行順序一致苛聘,同步的形式})。這樣一來代碼顯得十分簡潔忠聚,開發(fā)起來也方便多了设哗。

這里還有個(gè)技巧,我們發(fā)現(xiàn)WaitTime中需要將tcs.SetResult扔回到主線程執(zhí)行两蟀,微軟給我們提供了一種簡單的方法熬拒,在主線程設(shè)置好同步上下文

SynchronizationContext.SetSynchronizationContext(OneThreadSynchronizationContext.Instance);

在WaitTime中直接調(diào)用tcs.SetResult(true)就行了,回調(diào)會(huì)自動(dòng)扔到同步上下文中垫竞,而同步上下文我們可以在主線程中取出回調(diào)執(zhí)行,這樣自動(dòng)能夠完成回到主線程的操作蛀序。

? ? ? private static int loopCount = 0;

? ? ? ? static void Main(string[] args)

? ? ? ? {

? ? ? ? ? ? //OneThreadSynchronizationContext _ = OneThreadSynchronizationContext.Instance;

? ? ? ? ? ? // 微軟提供同步上下文

? ? ? ? ? ? SynchronizationContext.SetSynchronizationContext(OneThreadSynchronizationContext.Instance);

? ? ? ? ? ? //WaitTimeAsync(5000,WaitTimeFinishCallback);

? ? ? ? ? ? Console.WriteLine($"主線程:{Thread.CurrentThread.ManagedThreadId}");

? ? ? ? ? ? Crontine();

? ? ? ? ? ? while (true)

? ? ? ? ? ? {

? ? ? ? ? ? ? ? OneThreadSynchronizationContext.Instance.Update();

? ? ? ? ? ? ? ? Thread.Sleep(1);

? ? ? ? ? ? ? ? ++loopCount;

? ? ? ? ? ? ? ? if (loopCount % 10000==0)

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? Console.WriteLine($"loop count:{loopCount}");

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? private static async void Crontine()

? ? ? ? {

? ? ? ? ? ? await WatiTimeAsync(5000);

? ? ? ? ? ? Console.WriteLine($"1當(dāng)前線程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");

? ? ? ? ? ? await WatiTimeAsync(4000);

? ? ? ? ? ? Console.WriteLine($"2當(dāng)前線程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");

? ? ? ? ? ? await WatiTimeAsync(3000);

? ? ? ? ? ? Console.WriteLine($"3當(dāng)前線程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");

? ? ? ? }

? ? ? ? private static Task WatiTimeAsync(int waitTime)

? ? ? ? {

? ? ? ? ? ? TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

? ? ? ? ? ? Thread thread = new Thread(()=> WaitTime(waitTime,tcs)); // 開啟一個(gè)線程

? ? ? ? ? ? thread.Start();

? ? ? ? ? ? return tcs.Task;

? ? ? ? }

? ? ? ? /// <summary>

? ? ? ? /// 另外一個(gè)線程操作

? ? ? ? /// </summary>

? ? ? ? /// <param name="waitTime"></param>

? ? ? ? /// <param name="tcs"></param>

? ? ? ? private static void WaitTime(int waitTime,TaskCompletionSource<bool> tcs)

? ? ? ? {

? ? ? ? ? ? Thread.Sleep(waitTime);

? ? ? ? ? ? // 已經(jīng)設(shè)置同步上下文

? ? ? ? ? ? tcs.SetResult(true);

? ? ? ? ? ? // 將tcs扔回主線程

? ? ? ? ? ? //OneThreadSynchronizationContext.Instance.Post(o=>tcs.SetResult(true),null);

? ? ? ? }


執(zhí)行結(jié)果

如果不設(shè)置同步上下文欢瞪,你會(huì)發(fā)現(xiàn)打印出來當(dāng)前線程就不是主線程,這也是很多第三方庫跟.net core內(nèi)置庫的用法徐裸,默認(rèn)不回調(diào)到主線程遣鼓,所以我們使用的時(shí)候需要設(shè)置下同步上下文。其實(shí)這個(gè)設(shè)計(jì)本人覺的沒有必要重贺,交由庫的開發(fā)者去實(shí)現(xiàn)更好骑祟,尤其是在游戲開發(fā)中回懦,邏輯全部是單線程的,回調(diào)每次都走一遍同步上下文顯得多余了次企,所以ET框架提供了不使用同步上下文的實(shí)現(xiàn)ETTask,diam更顯的簡潔高效怯晕。

注:

async:\color{red}{使用 async 修飾符可將方法、lambda 表達(dá)式或匿名方法指定為異步缸棵。 如果對(duì)方法或表達(dá)式使用此修飾符舟茶,則其稱為異步方法 }

await:\color{red}{await 運(yùn)算符暫停對(duì)其所屬的 async 方法的求值堵第,直到其操作數(shù)表示的異步操作完成吧凉。 異步操作完成后,await 運(yùn)算符將返回操作的結(jié)果(如果有)}

task:\color{red}{表示一個(gè)可以返回值的異步操作踏志。}

TaskCompletionSource<TResult>:\color{red}{表示未綁定到委托的 Task<TResult> 的制造者方阀捅,并通過 Task 屬性提供對(duì)使用者方的訪問。}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末针余,一起剝皮案震驚了整個(gè)濱河市饲鄙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌涵紊,老刑警劉巖傍妒,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異摸柄,居然都是意外死亡颤练,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門驱负,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嗦玖,“玉大人,你說我怎么就攤上這事跃脊∮畲欤” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵酪术,是天一觀的道長器瘪。 經(jīng)常有香客問我,道長绘雁,這世上最難降的妖魔是什么橡疼? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮庐舟,結(jié)果婚禮上欣除,老公的妹妹穿的比我還像新娘。我一直安慰自己挪略,他們只是感情好历帚,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布滔岳。 她就那樣靜靜地躺著,像睡著了一般挽牢。 火紅的嫁衣襯著肌膚如雪谱煤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天卓研,我揣著相機(jī)與錄音趴俘,去河邊找鬼。 笑死奏赘,一個(gè)胖子當(dāng)著我的面吹牛寥闪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播磨淌,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼疲憋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了梁只?” 一聲冷哼從身側(cè)響起缚柳,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎搪锣,沒想到半個(gè)月后秋忙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡构舟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年灰追,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狗超。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡弹澎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出努咐,到底是詐尸還是另有隱情苦蒿,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布渗稍,位于F島的核電站佩迟,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏竿屹。R本人自食惡果不足惜音五,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望羔沙。 院中可真熱鬧,春花似錦厨钻、人聲如沸扼雏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诗充。三九已至,卻和暖如春蝴蜓,著一層夾襖步出監(jiān)牢的瞬間碟绑,已是汗流浹背茎匠。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留诵冒,地道東北人凯肋。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像侮东,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子豹芯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350