C# dotnet 使用 TaskCompletionSource 让事件转异步方法

2020-07-06 11:02:44 浏览数 (1)

咱今天来聊聊一个毁灭世界的故事,哦,不,是一个使用 TaskCompletionSource 让偷核武器,哦,又说错了,是让事件转换为异步的方法,让咱可以在一个方法里面顺序写下毁灭世界的逻辑

故事的背景是这个世界上的核导弹的发射是只要有密码就能发射,而刚好咱有一个强大的黑客团队可以窃取到密码。咱想要写一个方法,这个方法可以按照顺序发布一些指令,包括让黑客团队窃取密码,然后发射导弹,等待世界毁灭

因为黑客团队都很神秘,请动黑客团队去窃取密码之后,不会从原先的方法返回。而是黑客团队为了安全性(世界都要毁灭了,哪来的安全性) 会通过比特信告诉你,拿到密码了。等等,什么是比特信?这是一个超级高加密的匿名的邮箱 P2P 服务,反正对咱程序来说就是一个事件,定义如下

代码语言:javascript复制
    static class BtcMessage
    {
        public static event EventHandler KeyReceived;
    }

    static class HackTeam
    {
        public static void PeekKey()
        {

        }
    }

如上面的定义,黑客团队 HackTeam 有一个公开的方法是窃取密码,具体做啥,神秘的黑客可不会告诉咱。只是知道在完成之后,咱 BtcMessage 的 KeyReceived 事件将会触发。咱可以进行下一步的行动

下一步就是发射导弹了,尽管发射的过程很复杂,但对咱来说也是一个函数调用的事情

代码语言:javascript复制
    static class Missiles
    {
        public static void Fire()
        {

        }
    }

发射完成导弹之后,是不是这个世界就毁灭了?还不是,还需要等待导弹落地,爆炸,然后等待生态环境凉凉…… 没关系,最后这个世界会告诉咱世界已经毁灭了,因为这个世界是有意识的

代码语言:javascript复制
    static class World
    {
        public static event EventHandler Broke;
    }

好,那么咱的逻辑可以如何组织。第一步让黑客团队获取密码,等待 BtcMessage 事件回调。在 BtcMessage 事件触发之后调用导弹发射。然后等待世界触发毁灭事件

按照最简单的逻辑应该是这样写的,本来是想做个 WPF 程序,点击按钮就执行毁灭世界的。不过看看还是控制台简单

代码语言:javascript复制
        static void Main(string[] args)
        {
            World.Broke  = (sender, eventArgs) =>
            {
                Console.WriteLine("Hello World!");
            };

            BtcMessage.KeyReceived  = (sender, eventArgs) =>
            {
                Missiles.Fire();
            };

            HackTeam.PeekKey();
        }

可以看到代码不清真,因为都是倒过来写的,不倒过来也不成,因为如果在动作执行之后再监听事件,说不定事件就执行完成了

这就需要标题的 TaskCompletionSource 出场了,这个类的作用就是支持等待,同时可以被设置完成,很不好理解。写写代码就知道了

添加了 TaskCompletionSource 的写法如下

代码语言:javascript复制
            HackTeam.PeekKey();

            await btcReceivedTask.Task;

            Missiles.Fire();

            await worldBrokeTask.Task;

            Console.WriteLine("Hello World!");

是的按照顺序了写下来了,但是 btcReceivedTask 和 worldBrokeTask 是什么?这是先准备好的 TaskCompletionSource 对象

代码语言:javascript复制
            var btcReceivedTask = new TaskCompletionSource<bool>();
            var worldBrokeTask = new TaskCompletionSource<bool>();

            BtcMessage.KeyReceived  = (sender, eventArgs) =>
            {
                btcReceivedTask.SetResult(true);
            };

            World.Broke  = (sender, eventArgs) =>
            {
                worldBrokeTask.SetResult(true);
            };

为什么需要使用 TaskCompletionSource<bool> 是因为 TaskCompletionSource 只有泛形的版本,而 SetResult 方法一调用,将会让等待的代码继续往下执行

也就是在代码执行到 await btcReceivedTask.Task; 的时候,将会进入等待。在 btcReceivedTask.SetResult 方法被调用之后,才会继续执行 await btcReceivedTask.Task; 之后的代码

于是在 TaskCompletionSource 的辅助之后的代码,写毁灭世界的逻辑请看来就清真了

当然,一开始的代码还可以封装一下,咱可以封装出等待任意事件的触发作为异步的代码

例如封装一个世界被毁灭的等待任务

代码语言:javascript复制
    public class WorldBrokeTask
    {
        public WorldBrokeTask()
        {
            World.Broke  = World_Broke;
        }

        private void World_Broke(object sender, EventArgs e)
        {
            World.Broke -= World_Broke;
            TaskCompletionSource.SetResult(true);
        }

        public Task WaitWorldBroke()
        {
            return TaskCompletionSource.Task;
        }

        private TaskCompletionSource<bool> TaskCompletionSource { get; } = new TaskCompletionSource<bool>();
    }

这个封装可以协助咱准备监听是哪一次的世界毁灭,这样咱就不需要处理具体的事件监听逻辑。如一开始的代码其实存在一个坑就是当在毁灭世界之后,下一次世界毁灭的时候,又会触发事件。如果不是创建方法,那么很难做到只监听一次

通过封装之后的使用如下

代码语言:javascript复制
    var worldBrokeTask = new WorldBrokeTask();

    HackTeam.PeekKey();

    await btcReceivedTask.Task;

    Missiles.Fire();

    await worldBrokeTask.WaitWorldBroke();

    Console.WriteLine("Hello World!");

可以看到创建出来 WorldBrokeTask 然后接着等待就可以了,代码很简单

通过本文的例子相信大家也掌握了毁灭世界,哦,不,使用 TaskCompletionSource 封装事件为异步的方法

当然本文也回答了一个问题,是否使用 await 就存在线程的切换。其实可以不用切换线程

0 人点赞