在最近的项目中思考了以下这种需求,觉得很有必要仔细琢磨下:

当用户连续不断的重复某种行为或发出重复的请求时,服务如何能保证此种请求按用户行为的顺序来执行呢?

举个例子,比如用户在1s内连续按下了100次保存按钮(用户有时候确实很厉害呢... 那么这100次保存请求如何被有序的执行并不阻塞用户界面呢?(也就是用户可以继续按233333

答案很简单,同步(抽 要是同步就解决的话就没这问题了,一个请求执行起来稍微费点时间同步就不是选择了,用户在发一次请求之后就会被请求执行的时间所阻塞,用户体验X

那么并发后使用lock呢?这个我并不是没有实验过,也许是答案之一,但问题在于若此种请求耗费时间过长,服务会根据用户请求的次数来不断建立并发Task,完成这些请求的速度是上去了,但内存会不断增加,最后连整个服务都崩掉也不是不可能

最简单的办法就是不断使用一个Task来串行的执行请求,这样既不会阻塞用户界面也不会因为用户的操作而使资源的占用疯狂膨胀而产生问题。

实际上最优的解决方案应该是串行与并行的组合,控制产生Task(实际上就是线程)的数量,串行起几个并行的Task,几个并行的Task里面再去控制共享资源的同步。

这样是弹性最高的,比如预测请求执行的时间少于500毫秒时完全串行化,多于xxx时去实行一种组合。

 

说了那么多到底如何实现Task的串行化呢?说起串行化首先想到的是Task去做嵌套,但是这是种非常不优雅的做法,而且实际用起来会有很多限制,甚至需要通过事件机制与轮询来实现

而一种普遍做法是.NET4.5里新增的await运算符或基于此原理的实现,那么4.0中有没有简单的实现方法呢?其实应用Task的ContinueWith方法就足以了,只需要一行很偷鸡取巧的代码...

 

不多贫了,直接上代码

    public class SerialTask<T>
    {
        Queue<T> queue = new Queue<T>();
        Task task;
        Action<T> action;

        public SerialTask(Action<T> serialaction)
        {
            action = serialaction;
            task = new Task(() => StartTask());
            task.Start();
        }

        public void SerialExecute(T quest)
        {
            queue.Enqueue(quest);
            Console.WriteLine("主线程继续运行" + Thread.CurrentThread.ManagedThreadId.ToString());
            task = task.ContinueWith(ContinueTask);
        }

        void StartTask()
        {
            Console.WriteLine("开始任务所用线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }

        void ContinueTask(Task task)
        {
            Console.WriteLine("继续任务所用线程:" + Thread.CurrentThread.ManagedThreadId.ToString());

            if (queue.Count == 0)
            {
                return;
            }
            
            action(queue.Dequeue());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("主线程:"  + Thread.CurrentThread.ManagedThreadId.ToString());

            Action<int> a = (i) =>
                {
                    // 模拟耗时操作
                    Thread.Sleep(555);
                    Console.WriteLine(i.ToString());
                };

            SerialTask<int> st = new SerialTask<int>(a);

            for (int i = 0; i < 55; i++)
            {
                st.SerialExecute(i);
            }

            Console.ReadKey();
        } 
    }

 

所谓偷鸡取巧的那行代码就是...

task = task.ContinueWith(ContinueTask);

让ContinueWith方法去引用自己来达到串行化的目的,大家可以通过修改阻塞的时间来看看任务是不是串行进行的,运行下例子就很清楚了

 

要是此行代码没有引用自身的话...

task.ContinueWith(ContinueTask);

那么会新建一个Task来执行,等于每次用户的请求都会新建一个Task(因为最初的Task早就执行完了),等于就几个Task并行在跑了,若此时没有同步共享资源执行的顺序是无法保证的,而且还可能出现异常=.=

 

实际上这个例子已经可以直接拿来使用了,通过传入Action(T)委托来参数化请求的执行,然后请求的次序问题是靠一个队列来保证的。拓展起来应该也容易,比如继承此类加个回调什么的~

 

posted on 2013-10-24 23:45  iou90  阅读(232)  评论(0编辑  收藏  举报