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é)成唬格,家破。
:
OneThreadSynchronizationContext類颜说,他繼承了SynchronizationContext類,而SyncehronizationContext提供在各種同步模型中傳播同步上下文的基礎(chǔ)公共汰聋。
。
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)改成同步形式()。這樣一來代碼顯得十分簡潔忠聚,開發(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);
? ? ? ? }
如果不設(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:。
await:
task:
TaskCompletionSource<TResult>: