.NET 使用WaitHandle开启并发多线程查询并同步返回

原因

最近在APP开发过程中,或者说在服务端的一些业务中,难免会遇到需要将一堆不同内容的查询结果放在一个list种进行返回,如果数据量大,查询内容多,对数据实时性要求高,仅仅是建立索引,或者建立服务器数据缓存机制,都不能从根本上解决单线程逐SQL语句查询时间漫长的问题。

串行查询

联想

无论是java,C#,JavaScript或者在OC中,都有多线程这个东西存在,之前对于多线程运用在这种多任务的处理上并不多,但是不禁想到,是不是也有一种办法,让所有的查询在子线程中去执行,然后等待所有的子线程查询结束,并且将每个查询的结果值添加到list中后,再从主线程中将list返回呢,这样就可以利用多线程的优势,极大的提高查询的效率,几乎能将查询时间缩短到耗时最长的一个查询耗时上,并且现今的电脑都是多核CPU,使用多线程来解决这种问题似乎是再好不过了。

并发查询

EventWaitHandle(等待事件句柄)

EventWaitHandle是一个在线程处理上的类,它可以和WaitHandle配合使用完成多线程任务等待调度,并且在主线程中统一处理想要的结果。

代码如下:

 private List<string> test()
        {
            List<string> list = new List<string>();
            //创建等待事件句柄集合
            var watis = new List<EventWaitHandle>();
            for (int i = 0; i < 5; i++)
            {
                //创建句柄   true终止状态
                var handler = new ManualResetEvent(false);
                watis.Add(handler);
                //将要执行的方法参数化
                ParameterizedThreadStart start = new ParameterizedThreadStart(selectData);
                //创建线程,传入线程参数
                Thread t = new Thread(start);
                //启动线程
                t.Start(new Tuple<int, EventWaitHandle, List<string>>(i, handler,list));
            }
            //等待句柄
            WaitHandle.WaitAll(watis.ToArray());
            return list;
        }
  • 首先创建了一个EventWaitHandle的list,这个list将用于来添加所有的需要执行的等待事件句柄

  • 然后将需要参与等待的任务(一个方法)参数化传入线程初始化的构造

  • 在线程启动时,将与之对应的EventWaitHandle子类ManualResetEvent的对象传入需要调用的任务(方法)中

  • 最后使用WaitHandle.WaitAll执行所有的等待事件句柄

 private static void selectData(object param)
        {
            Tuple<int, EventWaitHandle,List<string>> t = (Tuple<int, EventWaitHandle,List<string>>)param;
            Console.WriteLine(Thread.CurrentThread.Name + "执行开始");
            //sleep线程,模拟查询业务
            if (t.Item1 == 0)
            {
                Thread.Sleep(1500);
                t.Item3.Add("这是第0个线程添加的内容");
            }
            else if (t.Item1 == 1)
            {
                Thread.Sleep(1234);
                t.Item3.Add("这是第1个线程添加的内容");
            }
            else if (t.Item1 == 2)
            {
                Thread.Sleep(1759);
                t.Item3.Add("这是第2个线程添加的内容");
            }
            //将事件状态设置为有信号,从而允许一个或多个等待线程继续执行。
            t.Item2.Set();
            Console.WriteLine(Thread.CurrentThread.Name + "执行结束");
        }
  • 在等待句柄任务中执行查询,并将结果加入数据list中

  • 最后在任务的最后(执行完成)将等待事件句柄对象Set(),这个方法将发出一个信号(暂时理解为通知WaitHandle当前的等待事件句柄执行完成)

处理结果

可以看出耗费的时间基本上为耗时最长的那个任务的耗时,可以想象,如果是在做多查询结果集的时候,带来的速度上的提升肯定是成倍的增长。

实际运用效果

提示10倍以上速度

博主最近参与开发的APP中有一个接口是需要从两个不同的数据库中进行数据查询,其中需要对查询数据库1中的返回数据进行遍历,然后根据遍历获取的数据去查询数据库2中的数据,而查询数据库2中的数据一次耗时在170ms左右,一次查询10条数据,整个接口查询将近用时1800ms,经过优化后,使用多线程并发查询,查询时间缩短在200ms以内,提示的效果非常显著。

多线程调试是一个非常头疼的事情,断点并不会按照当前进入断点的线程走(会乱跳),因为是多线程进行,会按照CPU抢占的实际情况进断点,所以建议调试的时候使用debug输出调试,并且在多线程断点的时候其它未被断点的线程会执行完成,而你断点的线程却还停留在你断点的位置。

多线程并发查询还需要考虑数据库的锁问题,虽然数据库一般对查询语句不会加锁,然而博主在实际使用过程中,ADO.net似乎是对查询进行了加锁,让并发到数据库的时候变成了串行,这个时候可以在查询的sql语句中添加with nolock,当然,并发查询肯定也会被其它操作的锁所影响,如果是在查询过程中还进行了其它的操作,有可能会查询出脏数据。

思考

使用等待句柄以防止进程在等待后台线程未完成执行时终止,实现根据官网的文档说明是使用的代理模式,猜想在调用的Wait方法中,应该有存在类似while(true)这样的循环或者其它阻塞当前线程的方法存在,这个存在一直阻止着当前主线程的在Wait中不返回,直到等待句柄调用了set方法时发出信号通知WaitHandle,将Wait方法中累计while循环的结束的条件,当前所有的等待句柄都发出信号后,Wait方法中while循环终止条件满足,不再继续阻塞当前主线程进行返回。

有不对的地方,请指出。(END)