zl程序教程

您现在的位置是:首页 >  后端

当前栏目

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

c#异步 优化 分享 方案 多种 委托
2023-06-13 09:15:14 时间

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

假设有四个任务:

任务1:登陆验证(CheckUser)

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

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

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

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

复制代码代码如下:


newThread(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:

复制代码代码如下:


privatestaticvolatilebool_m2;//任务2的标志位
 privatestaticvolatilebool_m3;//任务3的标志位
 privatestaticvoidMain(string[]args)
       {
           newThread(delegate
               {
                   CheckUser();
                   newThread(delegate
                       {
                            GetDatFromDb();//从数据库获取数据
                            _m2=true;//标志位置为true
                       }).Start();
                   newThread(delegate
                       {
                           GetDataFromWeb();//web服务获取数据
                           _m3=true;//标志位置为true
                       }).Start();
                   newThread(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,代码如下

复制代码代码如下:
privatestaticvoidMain(string[]args)
       {
           newThread(delegate
               {
                   CheckUser();
                   newThread(delegate
                       {
                           Threadtask2=newThread(delegate
                               {
                                   GetDatFromDb();//从数据库获取数据
                               });
                           Threadtask3=newThread(delegate
                               {
                                   GetDataFromWeb();//web服务获取数据
                               });
                           task2.Start();
                           task3.Start();
                           task2.Join();//任务2阻塞
                           task3.Join();//任务3阻塞
                           StartProcess();//执行任务4
                       }).Start();
               }).Start();
       }


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

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

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

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

复制代码代码如下:
privatestaticEventWaitHandleeventWait1=newEventWaitHandle(false,EventResetMode.AutoReset);//初始化状态false;
       privatestaticEventWaitHandleeventWait2=newEventWaitHandle(false,EventResetMode.AutoReset);//初始化状态false;
       privatestaticvoidMain(string[]args)
       {
           newThread(delegate
               {
                   CheckUser();
                    newThread(delegate
                   {
                       GetDatFromDb();//从数据库获取数据
                       eventWait1.Set();//标志位置为true
                   }).Start();
                  newThread(delegate
                   {
                       GetDataFromWeb();//web服务获取数据
                       eventWait2.Set();//标志位置为true
                   }).Start();
                   newThread(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。

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

复制代码代码如下:
privatestaticobjectobj=newobject();
       privatestaticvolatilebool_m2;//任务2的标志位
       privatestaticvolatilebool_m3;//任务3的标志位

       privatestaticvoidMain(string[]args)
       {
           CheckUser();//第一步验证用户
           Actionstep2=delegate
           {
               GetDatFromDb();//从数据库获取数据
               _m2=true;//标志位置为true
           };
           Actionstep3=delegate
           {
               GetDataFromWeb();//web服务获取数据
               _m3=true;//标志位置为true
           };

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

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

复制代码代码如下:
delegate
           {
               if(_m2&&_m3)//通过标志位判断23是否都已完成
               {
                   lock(obj)
                   {
                       _m3=false;
                       if(_m2)
                           StartProcess();//执行4
                   }
               }
           }

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

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

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