ET笔记_协成_异步

2.1 什么是协成

(黑色字体为熊猫,猫大原话。红色自己理解)

说到协成,我们先了解什么是异步,异步简单来说就是,我要发起一个调用,但是这个被调用方(可能是其他线程,也可能是IO)出结果需要一段时间,我不想这个调用阻塞竹调用方的整个线程,因此传给被调用方一个回调函数,被调用方运行完成后回调这个回调函数就能通知调用方继续往下执行。

static void Main(string[] args)

        {

            int loopCount = 0;

            while (true)

            {

                Thread.Sleep(1);

                ++loopCount;

                if (loopCount % 10000==0)

                {

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

                }

            }

        }

这时我需要加个功能,在程序一开始,我希望在5秒钟之后打印出loopCount的值。看到5秒后我们可以想到Sleep方法,它会阻塞线程一定时间然后继续执行。我们显然不能在主线程中Sleep,因为会破坏掉每10000次计数打印一次的逻辑。

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>

        /// 回调函数

        /// </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);

        }

我们这里设计了一个WaitTimeAsync方法,WaitTimeAsync其实就是一个典型的异步方法,它从竹线程发起调用,传入了一个WaitTimeFinishCallback回调方法做参数,开启了一个线程,线程Sleep一定时间后,将传过来的回调扔回到竹线程程序执行。OneThreadSynchronizationContext是一个跨线程队列,任何线程可以往里面扔委托。OneThreadSynchronizationContext的Update方法在主线程中调用,会将这些委托取出来放在主线程执行,为什么回调方法需要扔回到主线程执行呢?隐晦回调方法中读取了loopCount,loopCount在主线程中也有读写,所以要么加锁,要么永远保证只在主线程中读取。加锁是个不好的做法,代码中到处都是锁会导致阅读跟维护困难,很容易产生多线程bug。这中将逻辑打包成委托然后扔回另外一个线程多线程开发中常用的技巧。

我们可能又需要改动需求,WaitTimeFinishCallback执行完成之后,再想等3秒,再打印一下loopCount.

      /// <summary>

        /// 回调函数

        /// </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();

        }

我们这是可能仍然需要改需求,三秒后继续,接下来四秒继续打印。这样的话如同上面我们还需要写个回调函数,在三秒结束后调用。这样插入代码,显得非常繁琐。这里可以回答什么是协成,\color{green}{这样一串串回调就是协程}

\color{red}{OneThreadSynchroniationContext}:

OneThreadSynchronizationContext类,他继承了SynchronizationContext类,而SyncehronizationContext提供在各种同步模型中传播同步上下文的基础公共。

\color{red}{OneThreadSynchronizationContext:收集各个线程的回调方法,并且放回到主线程进行}

public class OneThreadSynchronizationContext:SynchronizationContext

    {

        /// <summary>

        /// 单列

        /// </summary>

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

        /// <summary>

        /// 线程Id

        /// </summary>

        private readonly int mainThreadid = Thread.CurrentThread.ManagedThreadId;

        /// <summary>

        ///  线程同步队列 (回调的方法)

        /// </summary>

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

        /// <summary>

        /// 委托

        /// </summary>

        private Action a;

        public void Update()

        {

            while (true)

            {

                // 出队成功,执行委托。否则结束这次判断

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

                {

                    return;

                }

                a();

            }

        }

        /// <summary>

        /// 回调方法如队

        /// </summary>

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

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

        public override void Post(SendOrPostCallback callback, object state)

        {

            // 当前线程是主线程这直接执行回调函数,否则加入回调队列

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

            {

                callback(state);

                return;

            }

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

        }

    }

更好的协成:异步

上文讲了一串回调就是协成,显然这样写代码,增加逻辑,插入逻辑非常容易出错。我们需要利用异步语法把这个异步回调的形式改成同步的像是,幸好C#已经帮我们设计好了。

      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当前线程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");

            await WatiTimeAsync(4000);

            Console.WriteLine($"2当前线程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");

            await WatiTimeAsync(3000);

            Console.WriteLine($"3当前线程:{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)); // 开启一个线程

            thread.Start();

            return tcs.Task;

        }

        /// <summary>

        /// 另外一个线程操作

        /// </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参数,WaitTimeAsync方法反悔了一个Task类型的结果。WaitTime中我们把action()替换成了tcs.SetResult(true),WaitTimeAsync方法钱使用await关键字,这样可以将一连串的回调改成同步形式(\color{red}{await阻塞,这样就会从上往下执行,这样开起来和我们平时开发执行顺序一致,同步的形式})。这样一来代码显得十分简洁,开发起来也方便多了。

这里还有个技巧,我们发现WaitTime中需要将tcs.SetResult扔回到主线程执行,微软给我们提供了一种简单的方法,在主线程设置好同步上下文

SynchronizationContext.SetSynchronizationContext(OneThreadSynchronizationContext.Instance);

在WaitTime中直接调用tcs.SetResult(true)就行了,回调会自动扔到同步上下文中,而同步上下文我们可以在主线程中取出回调执行,这样自动能够完成回到主线程的操作。

      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当前线程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");

            await WatiTimeAsync(4000);

            Console.WriteLine($"2当前线程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");

            await WatiTimeAsync(3000);

            Console.WriteLine($"3当前线程:{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)); // 开启一个线程

            thread.Start();

            return tcs.Task;

        }

        /// <summary>

        /// 另外一个线程操作

        /// </summary>

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

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

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

        {

            Thread.Sleep(waitTime);

            // 已经设置同步上下文

            tcs.SetResult(true);

            // 将tcs扔回主线程

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

        }


执行结果

如果不设置同步上下文,你会发现打印出来当前线程就不是主线程,这也是很多第三方库跟.net core内置库的用法,默认不回调到主线程,所以我们使用的时候需要设置下同步上下文。其实这个设计本人觉的没有必要,交由库的开发者去实现更好,尤其是在游戏开发中,逻辑全部是单线程的,回调每次都走一遍同步上下文显得多余了,所以ET框架提供了不使用同步上下文的实现ETTask,diam更显的简洁高效。

注:

async:\color{red}{使用 async 修饰符可将方法、lambda 表达式或匿名方法指定为异步。 如果对方法或表达式使用此修饰符,则其称为异步方法 }

await:\color{red}{await 运算符暂停对其所属的 async 方法的求值,直到其操作数表示的异步操作完成。 异步操作完成后,await 运算符将返回操作的结果(如果有)}

task:\color{red}{表示一个可以返回值的异步操作。}

TaskCompletionSource<TResult>:\color{red}{表示未绑定到委托的 Task<TResult> 的制造者方,并通过 Task 属性提供对使用者方的访问。}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270