c#并行任务多种优化方案分享(异步委托)

遇到一个多线程任务优化的问题,现在解决了,分享如下。

假设有四个任务:

任务1:登陆验证(CheckUser)

任务2:验证成功后从Web服务获取数据(GetDataFromWeb)

任务3:验证成功后从数据库获取数据(GetDatFromDb)

任务4:使用2、3的数据执行一个方法 (StartProcess)

一个比较笨的方法(本人最开始的方法,记为方法1)是直接开启一个线程,按照顺序依次执行四个任务:

new Thread(delegate
                {
                    CheckUser();
                    GetDatFromDb();//从数据库获取数据
                    GetDataFromWeb();//web服务获取数据
                    StartProcess();//执行4
                }).Start();

但是仔细分析需求我们会发现,任务2和任务3并没有先后区别,事实上两者并无关联,只不过任务4的执行需要任务2和3都已完成作为条件,所以我们可以再开两个线程用于执行任务2和任务3,当两者都执行完毕之后,执行任务4。

在这里使用了两个全局变量用于表示任务2和任务3的状态。用三个线程分别执行任务2、3、4,其中任务4一直在循环监听全局变量的状态,确保在2、3都执行完毕后才执行。

这记为方法2:

private static volatile bool _m2;//任务2的标志位
 private static volatile bool _m3;//任务3的标志位
 private static void Main(string[] args)
        {
            new Thread(delegate
                {
                    CheckUser();
                    new Thread(delegate
                        {
                             GetDatFromDb();//从数据库获取数据
                             _m2 = true;//标志位置为true
                        }).Start();
                    new Thread(delegate
                        {
                            GetDataFromWeb();//web服务获取数据
                            _m3 = true;//标志位置为true
                        }).Start();
                    new Thread(delegate
                        {
                            while (!(_m3 && _m2))//判断任务2和3是否已执行完毕
                            {
                                Thread.Sleep(100);
                            }
                            StartProcess();//执行任务4
                            _m2 = true;
                        }).Start();
                }).Start();
          }

以上代码基本上已经可以达到预期目标了,但是由于借助了两个全局变量,尽管在这里不会涉及到同步冲突的问题,但总觉得很不放心,而且当我们需要做扩展的时候,比方说在执行任务4之前,我们还需要加载文件内的数据(GetDataFromFile),那我们必须再添加一个全局标志位,显得有点麻烦。

事实上,Thread类本身已经拥有对这种情况的完美解决方案——join。

Thread.Join 方法
在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻塞调用线程,直到某个线程终止为止。

简单来说,join就是个阻塞方法,在线程1内创建线程2,调用线程2的Join方法,那么线程1将会被阻塞,直到线程2执行完毕。运用到上面的例子就是,在任务4内创建任务2和任务3的线程,调用任务2和任务3的线程的Join方法使任务4阻塞,直到任务2和任务3执行完毕,才继续执行任务4。这记为方法3,代码如下

private static void Main(string[] args)
        {
            new Thread(delegate
                {
                    CheckUser();
                    new Thread(delegate
                        {
                            Thread task2 = new Thread(delegate
                                {
                                    GetDatFromDb(); //从数据库获取数据
                                });
                            Thread task3 = new Thread(delegate
                                {
                                    GetDataFromWeb(); //web服务获取数据
                                });
                            task2.Start();
                            task3.Start();
                            task2.Join();//任务2阻塞
                            task3.Join();//任务3阻塞
                            StartProcess(); //执行任务4
                        }).Start();
                }).Start();
        }

这样便不需要任何标志位了。这是最理想的解决方案。

另外还有一种解决方案,使用EventWaitHandle

EventWaitHandle 类允许线程通过发出信号和等待信号来互相通信。事件等待句柄(简称事件)就是可以通过发出相应的信号来释放一个或多个等待线程的等待句柄。信号发出后,可以用手动或自动方式重置事件等待句柄

简单来说,就是方法2的进阶版,使用EventWaitHandle控制状态,而不再使用While循环监听,但这里仍旧需要两个全局的EventWaitHandle对象。该方法记为方法4,代码如下

private static EventWaitHandle eventWait1 = new EventWaitHandle(false, EventResetMode.AutoReset);//初始化状态false;
        private static EventWaitHandle eventWait2 = new EventWaitHandle(false, EventResetMode.AutoReset);//初始化状态false;
        private static void Main(string[] args)
        {
            new Thread(delegate
                {
                    CheckUser();
                     new Thread(delegate
                    {
                        GetDatFromDb(); //从数据库获取数据
                        eventWait1.Set(); //标志位置为true
                    }).Start();
                   new Thread(delegate
                    {
                        GetDataFromWeb(); //web服务获取数据
                        eventWait2.Set(); //标志位置为true
                    }).Start();
                    new Thread(delegate
                        {
                            eventWait1.WaitOne();//任务2阻塞,等待
                            eventWait2.WaitOne();//任务3阻塞,等待
                            StartProcess(); //执行任务4
                        }).Start();
                }).Start();
        }

上述三个优化方案,其实核心思想都是一样的,都是通过开启3个线程分别执行2、3、4任务,其中任务4被阻塞(while循环、eventWait.WaitOne,thread.join),当阻塞解除后,继续执行任务4。也就是说,任务4,其实是一直在等待任务2和任务3的完成。那么,是否有办法让任务2和任务3主动通知任务4呢?即,任务2和任务3完成后,主动执行任务4。

方法当然有:异步委托+回调函数

private static object obj = new object();
        private static volatile bool _m2;//任务2的标志位
        private static volatile bool _m3;//任务3的标志位

private static void Main(string[] args)
        {
            CheckUser(); //第一步 验证用户
            Action step2 = delegate
            {
                GetDatFromDb(); //从数据库获取数据
                _m2 = true; //标志位置为true
            };
            Action step3 = delegate
            {
                GetDataFromWeb(); //web服务获取数据
                _m3 = true; //标志位置为true
            };

step2.BeginInvoke(delegate
            {
                if (_m2 && _m3) //通过标志位判断2 3是否都已完成
                {
                    lock (obj)//加锁
                    {
                        _m2 = false;
                        if (_m3)//二重验证 防止两者同时进入
                            StartProcess(); //执行4
                    }
                }
            }, null);
            step3.BeginInvoke(delegate
            {
                if (_m2 && _m3) //通过标志位判断2 3是否都已完成
                {
                    lock (obj)
                    {
                        _m3 = false;
                        if (_m2)
                            StartProcess(); //执行4
                    }
                }
            }, null);
        }

讲解下代码。首先以委托的方式创建了任务2和任务3的委托对象step2和step3。执行这两个委托的异步调用方法BegInvoke。执行BegInvoke,会创建一个新的线程来执行step2和step3的方法,同时,在执行BeginInvoke的时候还指定了一个回调函数

delegate
            {
                if (_m2 && _m3) //通过标志位判断2 3是否都已完成
                {
                    lock (obj)
                    {
                        _m3 = false;
                        if (_m2)
                            StartProcess(); //执行4
                    }
                }
            }

这个函数会在step2和step3的线程执行完毕后被调用。在这里,我再次使用了标志位来判断step2和step3是否已经运行完成,同时,为了防止一种特殊情:“step2和step3所执行的时间几乎相等,他们会同时通过if(_m2&&_m3)判断,进而执行两次StartProcess” 在这里加了lock锁,并且在lock锁内将标志位重置+二重判断(这里可以参考单例模式的双重锁定原理),确保StarProcess只会执行一次。

如此这般,一个主动通知模式的并行任务便实现了,不过,这种实现方法相较于方法2,实在太过麻烦,尤其在与并发处理方面,个人感觉实用性不太高。

另外,还可以使用观察者模式实现异步委托+回调函数的效果。

上一篇:Day15. 高级查询 一对多映射关系 -MyBatis高级


下一篇:pytest测试框架 -- skip跳过执行测试用例