zoukankan      html  css  js  c++  java
  • 一种简单的基于任务的事件异步处理的解决方案

    很多时候,为了界面更加友好,我们往往会设计这样一个功能:当用户在界面上输入一段内容时,无需点击确定按钮,后台自动对该输入进行处理,获取输出并显示出来

    要实现这个功能,一个最直接的方式是在TextBox的TextChanged事件中增加回调函数,在回调函数中获取返回值,并输出。这么乍看起来是一个比较好的方法,但有经验的程序员就马上会发现这样有几点不足:

    • 用户连输输入时,可能中间过程中的并不是用户的真正的输入,只是输入过程中产生的临时数据,对临时数据进行获取返回值的话,加大了处理负担。
    • TextChanged事件发生在UI线程,当获取返回值的处理函数时间较长的时候,阻塞UI线程,用户界面无响应。

    针对这两个问题,我们可以提出如下解决方案:

    • 不实时响应用户的输入,只有当用户停止输入一定时间(500ms)后,才开始进行获取返回值处理。
    • 获取返回值处理采用异步方案,在后台线程中获取返回值,返回值获取完成后,通知UI线程更新界面。

    这个方案本身没有什么问题,但这两个步骤都采用了异步操作,而异步操作往往把同步方案变得复杂的多了,带来了一系列要处理的新的问题,常见问题如下:

    1. 如何实现用户的输入延迟通知。
    2. 后台线程异步处理完成时,如何通知UI线程更新界面。
    3. 当某个输入操作的处理时间较长,导致新输入已经产生,但过期的结果可能覆盖新的输入的结果。例如:
      1. 用户先输入了一个google,后台启动线程1获取google的输出结果,但由于方校长发飙了,google被吓住了,一两秒还没有返回值;
      2. 用户等不及了,重新输入了个baidu,线程2很快就返回了baidu的输出结果,
      3. 此时线程1还在运行,再过了会儿,线程1返回了google的结果,重新刷新界面。这个时候,看到的输入时baidu,但界面上显示的结果是google,结果并不准确。
    4. 运行期间的错误如何处理

    这几个问题处理起来并不是很复杂,微软在MSDN文章Rx Hands-On Labs中就详述了通过RX解决这个问题的方案(其文档Rx .NET HOL介绍的非常详细,我本来想翻译下的,无奈时间不够,英语也太烂,推荐看本文的朋友读下):

    1. 通过Observable.FromEvent把TextChanged事件封装成IObservable事件源。
    2. 通过ObserveOn扩展函数处理UI线程上的结果更新
    3. 通过Switch函数过滤过期的输出
    4. 通过OnError函数处理异常

    该文档中还列举了其它几个问题的解决方案,由于较多,这里就不一一列举了。这种方式比较完善,但增加了一大堆回调函数和扩展函数,用起来不是很友好,对RX框架不熟悉的程序员来说也容易出错。

    在.Net 4.5中,引入了基于任务的异步编程模型,可以将以同步编程的方式实现异步处理。例如,对于前面的需求,我们可以以如下的方式实现:

        CancellationTokenSource currentCancelTokenSource = new CancellationTokenSource();

        input.TextChanged += (s, e) =>
            {
                currentCancelTokenSource.Cancel();        //
    取消当前正在执行的任务
                currentCancelTokenSource.Dispose();

                currentCancelTokenSource = new CancellationTokenSource();
                ProcessAsync(input.Text, currentCancelTokenSource.Token);
            };


        async void ProcessAsync(string input, CancellationToken cancel)
        {
            await Task.Delay(500);        //
    延迟
    500ms执行

            if (cancel.IsCancellationRequested)        //
    新的输入已经产生,任务取消
                return;

            try
            {
                var outputContent = await GetOutputAsync(input);

                if (cancel.IsCancellationRequested)        //
    本轮任务处理时间较长,后续的新的任务已经开始了。使用新任务的结果,本轮任务结果放弃。
                    return;

                output.Text = outputContent;
            }
            catch (Exception)
            {
                if (cancel.IsCancellationRequested)
                    return;

                output.Text = "
    运行出错
    ";
            }
                
        }    

    这种方式下,前面的几个问题处理全部集中在一个函数ProcessAsync中了,非常直观。和同步的方式比起来,主要改动有以下两点:

    1. 将以前的Process函数改名为ProcessAsync,增加了一个CancellationToken参数,用以查询当前输入的任务是否已经取消。
    2. 运行过程中加增加判断当前任务是否已经取消的操作,如果任务已经取消,则放弃本次操作的输出结果(包括异常)。

    这种方式用起来要友好的多,为了复用这种方式,我们需要把它封装下,一种方式是:通过Observable.FromEvent把TextChanged事件封装成IObservable事件源,然后通过我前文的IObservable的两个简单的扩展函数在Subscribe扩展函数中注册ProcessAsync回调。

    这种方式下,需要安装RX库,如果不想装RX库,可以使用我下面的这个轻量级的封装。

        class Notification<T>
        {
            Action<T, CancellationToken> hanlder;
            CancellationTokenSource currentCancelTokenSource = new CancellationTokenSource();

            public Notification(Action<T, CancellationToken> hanlder)
            {
                this.hanlder = hanlder;
            }

            public void Notify(T value)
            {
                currentCancelTokenSource.Cancel();        //
    取消当前正在执行的任务
                currentCancelTokenSource.Dispose();

                currentCancelTokenSource = new CancellationTokenSource();

                hanlder(value, currentCancelTokenSource.Token);
            }
        }

    这个类使用比较简单:

        var notify = new Notification<string>(ProcessAsync);
        input.TextChanged += (s, e) => notify.Notify(input.Text);

    当然,这个没有Observable.FromEvent那么友好,因此,我仿照Observable.FromEvent的功能增加了一个函数:

            //TODO 暂时没有考虑UnRegist,如果要支持UnRegist,把返回值改成IDisposable
            public static void RegistEventNofity(object obj, string eventName, Action<T, CancellationToken> hanlder)
            {
                EventInfo evt = obj.GetType().GetEvent(eventName, BindingFlags.Instance | BindingFlags.Public);
                var notify = new Notification<T>(hanlder);
                var eventInvoker = new EventPatten(arg => notify.Notify((T)arg));

                var method = typeof(EventPatten).GetMethod("EventOccured", BindingFlags.NonPublic | BindingFlags.Instance);
                evt.AddEventHandler(obj, Delegate.CreateDelegate(evt.EventHandlerType, eventInvoker, method));
            }

            class EventPatten
            {
                Action<object> hanlder;
                internal EventPatten(Action<object> hanlder) { this.hanlder = hanlder; }
                void EventOccured(object sender, object args) { hanlder(args); }
            }

    现在用起来就友好些了:

        Notification<TextChangedEventArgs>.RegistEventNofity(input, "TextChanged", (args, cancel) => ProcessAsync(input.Text, cancel));

    完整代码如下,欢迎有需要的朋友使用: 

    View Code 
        class Notification<T>
        {
            Action<T, CancellationToken> hanlder;
            CancellationTokenSource current = new CancellationTokenSource();

            public Notification(Action<T, CancellationToken> hanlder)
            {
                this.hanlder = hanlder;
            }

            public void Notify(T value)
            {
                current.Cancel();        //取消当前正在执行的任务
                current.Dispose();

                current = new CancellationTokenSource();

                hanlder(value, current.Token);
            }

            //TODO 暂时没有考虑UnRegist,如果要支持UnRegist,把返回值改成IDisposable
            public static void RegistEventNofity(object obj, string eventName, Action<T, CancellationToken> hanlder)
            {
                EventInfo evt = obj.GetType().GetEvent(eventName, BindingFlags.Instance | BindingFlags.Public);
                var notify = new Notification<T>(hanlder);
                var eventInvoker = new EventPatten(arg => notify.Notify((T)arg));

                var method = typeof(EventPatten).GetMethod("EventOccured", BindingFlags.NonPublic | BindingFlags.Instance);
                evt.AddEventHandler(obj, Delegate.CreateDelegate(evt.EventHandlerType, eventInvoker, method));
            }

            class EventPatten
            {
                Action<object> hanlder;
                internal EventPatten(Action<object> hanlder) { this.hanlder = hanlder; }
                void EventOccured(object sender, object args) { hanlder(args); }
            }
        }

    另外,由于WinRT的反射机制有所改变,如果要在WinRT环境下使用这个程序需要修改RegistEventNofity函数为如下形式:

    View Code
        public static void RegistEventNofity(object obj, string eventName, Action<T, CancellationToken> hanlder)
        {
            var evt = obj.GetType().GetRuntimeEvent(eventName);
            var notify = new Notification<T>(hanlder);
            var eventInvoker = new EventPatten(arg => notify.Notify((T)arg));
            var method = eventInvoker.GetType().GetRuntimeMethods().First(i => i.Name == "EventOccured");;
            var delegateType = evt.AddMethod.GetParameters()[0].ParameterType;

            evt.AddMethod.Invoke(obj, new object[] { method.CreateDelegate(delegateType, eventInvoker) });
        }
  • 相关阅读:
    乱谈服务器编程
    set global slow_query_log引起的MySQL死锁
    一个由string使用不当产生的问题
    Hbase初体验
    浅谈SQLite——查询处理及优化
    ACID、Data Replication、CAP与BASE
    libevent源码分析
    浅析Linux Native AIO的实现
    vim7.2中文乱码解决方法
    伸展树的点点滴滴
  • 原文地址:https://www.cnblogs.com/TianFang/p/2657439.html
Copyright © 2011-2022 走看看