65.9K
CodeProject 正在变化。 阅读更多。
Home

如何用您自己的方法覆盖父类事件方法?

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (7投票s)

2003年10月19日

5分钟阅读

viewsIcon

81778

downloadIcon

1168

使用Process类生成多个进程,并监视它们何时完成。

背景

在开发一个依赖数据库的免费Web产品时,我们不得不编写特定于数据库的C#作业,而这些作业通常会用T-SQL、PL/SQL或DB2 SQL编写。由于我们的公司不想再投资一台大型机器,我们需要从多个不同的工作站同时运行这些作业。数据库托管在一台SMP机器上,并通过Oracle 9i服务器的负载均衡故障转移功能,能够处理数千个并发连接。我们编写了一个竞速多线程C#批处理程序,以最大化CPU处理事务的能力。该程序在多个工作站上运行,这些工作站访问数据库服务器。工作站的数量经过调整,以使数据库服务器的CPU利用率保持在80%以下。我们编写了一个NT服务,以便从不同的工作站运行这些批处理程序。在单个工作站上,NT服务启动了多个进程,这些进程使用了不同的到Oracle负载均衡服务器的连接字符串。我们必须在进程中显式指定实例名,因为Oracle负载均衡服务器擅长平衡在线事务的负载,但不擅长批处理作业。换句话说,我们不是让一台大机器与另一台大机器通信,而是让几台小机器来“敲打”这台大机器。

引言

System.Diagnostic.Process类提供了在单独进程中运行程序的机制。我们需要生成同一个程序或NT作业脚本的多个进程。我们需要知道作业何时完成及其性能,因此我们编写了一个方法来捕获标准输出到特定于作业的文件,操作员可以查看该文件。通过批处理配置文件对工作站进行负载均衡,以优化每个进程花费的时间和每个进程的事务数量,这一点非常重要。

如果我们只想执行一个进程,那么通过将挂钩方法附加到Process.Exited事件处理程序并捕获进程的标准输出,就可以很容易地做到这一点。当进程完成时,Process.Exited事件处理程序会引发OnExit事件。这里的示例代码对此进行了说明。

Process p = new Process();
 
p.StartInfo.FileName = @"C:\WINDOWS\system32\ping.exe";
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.Arguments = @"www.google.com";
p.StartInfo.WorkingDirectory = @"C:\WINDOWS\system32";
p.EnableRaisingEvents = true;
p.StartInfo.UseShellExecute = false;
p.Exited += new EventHandler(captureStdout);
p.Start();
private void captureStdout(object sender, EventArgs e) 
{
    Process ps = (Process) sender;
    Console.WriteLine(ps.StandardOutput.ReadToEnd());
}

将对象sender类型转换为进程类型,将告诉您哪个进程已退出。如果您只运行一个进程,这甚至不是必需的,但在大多数情况下,如果您在一个循环中通过ArrayList处理多个进程,您会希望知道哪个进程已完成工作,知道其进程ID或名称。

我们真正需要的是比这更多一些,因为我们不仅要识别退出的进程,还要识别一些与进程属性无关的外部参数,例如Oracle负载均衡服务器的连接字符串以及我们希望为每个生成的进程标识的批处理名称。当进程退出时,我们应该能够捕获时间和其它外部属性,并将它们记录到数据库中,这样我们的Process Manager就可以同步工作,避免不同工作站运行同一个服务时发生冲突。

MSDN文档建议我们编写自己的事件处理程序来传递数据到事件。我们尝试从Process派生我们的类来重写OnExited API方法,但我们发现这个API方法在其基类中没有标记为virtualabstractoverride。我通过查看此基类的源代码证实了这一点。我不知道MSDN文档为何存在差异,或者是我遗漏了什么非常琐碎的事情?我不是C#专家,如果您了解,请告诉我。我们编写了一个单独的事件处理程序并将其附加到Exited事件处理程序,这样当父进程退出时,我们的自定义事件处理程序就会生效。

这个自定义事件处理程序允许您在事件处理程序的挂钩方法中访问进程的索引。如果没有它,您将不得不编写一个变通方法来缓存进程的索引,然后在进程退出时将其刷新。我们最初尝试了第二种方法,但它既麻烦又丑陋。

我们遵循的步骤如下:

  1. 创建一个自定义EventArgs类来传递事件数据。

    public class ProcessEventArgs : EventArgs
    {
      private readonly int index;
      public ProcessEventArgs (int p_index)
      {
        this.index = p_index; 
      }
      public int Index 
      {
        get {return index;}
      }
    }
  2. 创建一个委托。

    public delegate void 
      ProcessEventHandler(object sender, ProcessEventArgs e);
  3. 创建一个派生类,以便在父类引发OnExited事件时进行劫持。

          public class ProcessManager : System.Diagnostics.Process
          {
                private int i;
                ProcessEventHandler onProcessExited;
                public ProcessManager()
                {}
     
                public ProcessManager(int i)
                {
                      this.i = i; 
                      this.Exited += new 
                         EventHandler(processManagerCapture);
                }
     
                public event ProcessEventHandler ProcessExited
                {
                      add 
                      {
                            onProcessExited += value;
                      }
                      remove 
                      {
                            onProcessExited -= value;
                      }
                }
     
                public void OnProcessExited()
                {
                      if (onProcessExited != null)
                            onProcessExited(this,new ProcessEventArgs(i));
                }
     
                private void processManagerCapture(object o, EventArgs e)
                {
                      OnProcessExited();
                }
     
          }

    在上述派生类中,我们需要一种机制来在父事件OnExit引发时附加我们的事件。当我们实例化该类时,我们订阅基类的Exited处理程序,并将其附加到processManagerCapture方法。该方法会引发我们的自定义OnProcessExited事件。通过上述实现,您隐藏了自定义事件处理程序的功能,并允许开发人员使用派生类。

  4. 派生类的实现。

    我们使用ProcessExited事件处理程序将ArrayList的索引传递给退出方法。实现类显示了这一点。我们继承新类自ProcessManager,它实现了我们的自定义ProcessExited事件处理程序。BatchManager类在一个单独的进程中运行带有两个不同参数的ping程序,并捕获标准输出以及我们需要的外部属性。

    public class BatchManager : ProcessManager
    {
      ArrayList processArrayList;
      private string[] batchName = {"Batch 1","Batch 2"};
      private string[] pingSites = {"www.google.com","www.yahoo.com"};
     
      public BatchManager()
      {
      }
     
      public void Run()
      {
        try
        {
          RunPingProcess();
        }
        catch (Exception ex)
        {     
          Console.WriteLine(" Error while trying to " + 
            "Run ping process. Msg = " + ex.Message);
        }
      }
     
      private void RunPingProcess ()
      {
        processArrayList = new ArrayList();
        for (int i = 0; i < pingSites.Length; ++i)
        {
         processArrayList.Add(new ProcessManager(i));
         ((ProcessManager)processArrayList[i]).StartInfo.FileName = 
                     @"C:\WINDOWS\system32\ping.exe";
         ((ProcessManager)processArrayList[i]).StartInfo.RedirectStandardOutput
                                                                        = true;
         ((ProcessManager)processArrayList[i]).StartInfo.CreateNoWindow = true;
         ((ProcessManager)processArrayList[i]).StartInfo.Arguments = pingSites[i];
         ((ProcessManager)processArrayList[i]).StartInfo.WorkingDirectory = 
                         @"C:\WINDOWS\system32";
         ((ProcessManager)processArrayList[i]).EnableRaisingEvents = true;
         ((ProcessManager)processArrayList[i]).StartInfo.UseShellExecute = false;
         ((ProcessManager)processArrayList[i]).ProcessExited += 
         new ProcessEventHandler(NewCapture);
         ((ProcessManager)processArrayList[i]).Start();
        }
        for (int i = 0; i < pingSites.Length; ++i)
        {
          if (!((ProcessManager)processArrayList[i]).HasExited)
          {
            ((ProcessManager)processArrayList[i]).Refresh();
            ((ProcessManager)processArrayList[i]).WaitForExit();
          }
        }
      }
     
      private void NewCapture(object sender, ProcessEventArgs e)
      {
        ProcessManager pm = (ProcessManager) sender;
        Console.WriteLine("Batch Job " + batchName[e.Index] + ": Ping on " + 
        pingSites[e.Index] + " has completed.\nThe output of ping is:\n");
        Console.WriteLine(pm.StandardOutput.ReadToEnd());
        Console.WriteLine();
      }
    }

    我们的示例程序可以正常工作,因为我们的标准输出不大。但如果您的批处理进程有大量标准输出,上述挂钩方法将无法工作,需要进行修改。原因是,在主循环中我们等待进程退出,但挂钩方法会读取所有标准输出。由于内部实现是通过管道进行的,当缓冲区填满时,就会发生死锁。MSDN文档建议在调用WaitForExit之前读取标准输出。

  5. 作为独立程序运行。

    public class Test
    {
        public Test()
        {}
     
        static void Main()
        {
            BatchManager bm = new BatchManager();
            bm.Run();
        }
     
    }
© . All rights reserved.