<?xml version="1.0" encoding="GB2312" ?>
<?xml-stylesheet type="text/xsl" href="article.xsl" ?>

<article>

<title>使用.NET Remoting实现并行计算</title>

<authors>
  <author>Eric Bergman-Terrell</author>
</authors>

<techs>
  <tech>分布式</tech>
  <tech>并行计算</tech>
  <tech>.NET</tech>
  <tech>远程</tech>
</techs>

<difficulty>3</difficulty>

<copyright/>

<brief>
<paragraph>
过去，做一个并行计算的试验要费九牛二虎之力，今天，有了.NET Remoting，我们只需要完成非常少的编程工作，便可以跨多台计算机轻松进行分布计算。在本文中，Eric Bergman-Terrell创建了一个名为Digits of Pi的应用程序，它使用并行的多台计算机以不可思议的精度计算π值。他设法在12小时内完成了10,000位数的计算，却只使用了相当少的计算资源。这比用一台计算机单独完成计算快了300％。
</paragraph>
</brief>

<paragraph>
欢迎进入.NET Remoting的奇妙世界！在这篇文章里，您将与我一起，亲自动手体验并行计算的威力。为了方便您更好地理解这篇文章，请首先按照下面的步骤作一番准备：
<list order="true">
  <item>从附增光盘获取示例应用程序及源代码。</item>
  <item>打开Everything.sln解决方案。此解决方案包含运行“Digits of Pi”应用程序所需的三个项目（Client、Server和ServerLoader）。还包含一个名为SimpleClient的项目。加载Everything.sln之后，请选择Build（编译）| Batch Build...（批编译...）。单击Select All（全部选定）按钮，然后单击Build（编译）。编译所有内容后，请在本地计算机以及您的LAN中的远程计算机上安装该软件。</item>
  <item>在本地计算机上，创建一个文件夹并将以下文件复制到其中：</item>
  <list>
    <item>Server\bin\Release\Plouffe_Bellard.dll</item>
    <item>Client\bin\Release\DigitsOfPi.exe</item>
  </list>
  <item>在每个远程计算机和本地计算机上，创建一个文件夹并将以下文件复制到其中：</item>
  <list>
    <item>Server\bin\Release\Plouffe_Bellard.dll</item>
    <item>ServerLoader\bin\Release\ServerLoader.exe</item>
    <item>ServerLoader\ServerLoader.exe.config</item>
  </list>
  <item>然后运行ServerLoader.exe程序。当然，运行ServerLoader和Digits of Pi程序之前，需要在每台计算机上安装.NET Framework。</item>
</list>

</paragraph>

<paragraph>
在所有远程计算机和本地计算机上运行ServerLoader程序后，请运行Digits of Pi程序。单击Configure...（配置...）（参见图1），添加本地计算机名和远程计算机名。如果不确定某台计算机的名称，请查看ServerLoader程序，它在表中显示其计算机名。如果您很幸运地拥有一个多CPU系统，您只需为所有CPU输入一次计算机名。只需在计算机名后键入@符号和一个编号。例如，如果您拥有一个名为“Brainiac”的双CPU系统，则键入以下计算机名：“Brainiac@1”和“Brainiac@2”。为多CPU系统输入多个计算机名可以确保所有计算机的CPU都用于计算π值。输入所有计算机名后，单击OK（确定）。
<image src="DotNetRemoting1.jpg" desc="图1"/>
</paragraph>

<paragraph>
指定要计算的位数（参见图2）并单击Calculate（计算）。请从较少的位数开始，π值小数点后面的位数越多，程序所需的时间就越长。图3显示了Digits of Pi程序如何在本地计算机和远程计算机中分配工作量，它使用TCP/IP端口9000发送请求并接收结果。接下来，我们将详细探讨Remoting、Plouffe_Bellard服务器对象、ServerLoader程序、SimpleClient程序和Digits of Pi程序。
<image src="DotNetRemoting2.jpg" desc="图2"/>
<image src="DotNetRemoting3.jpg" desc="图3"/>
</paragraph>

<paragraph name="服务器对象">
服务器对象将计算指定的九位π值。它被命名为Plouffe_Bellard，因为它使用Fabrice Bellard的增强Simon Plouffe算法。虽然存在更快的算法，但Plouffe-Bellard算法非常简单（少于300行源代码），它使用少量的内存，并且由于九位数字可以单独计算，因此更适于并行执行。Plouffe_Bellard.CalculatePiDigits方法将计算在指定位置开始的九位π值。例如，CalculatePiDigits(0)从第一位开始返回九位数字：141592653。CalculatePiDigits(9)从第十位开始返回九位数字，依此类推。
</paragraph>

<paragraph name="ServerLoader">
ServerLoader程序将加载服务器对象，指定通过LAN访问服务器对象的协议和端口，侦听来自客户端程序的传入调用，处理调用并返回结果。特别值得注意的是，所有这些只需一行代码便可完成，只需通过使用配置文件的路径调用RemotingConfiguration.Configure方法。ServerLoader程序将加载名为ServerLoader.exe.config的配置文件（参见代码段1）。此配置文件指定以SingleCall模式加载服务器对象，即每个传入调用都由服务器对象的一个新实例处理。如果服务器对象以Singleton模式加载，每个传入调用都将由同一个实例处理。类型属性指定服务器对象的完整类型名称（包括PB命名空间）及其程序集的名称。objectUri属性指定对象的统一资源标识符(URI)的端点。&lt;channel&gt;元素指定使用TCP协议，端口9000访问服务器对象。代码段1：ServerLoader.exe.config
<code>
&lt;configuration&gt; 
  &lt;system.runtime.remoting&gt;  
    &lt;application name = "ServerLoader"&gt;  
      &lt;service&gt; 
        &lt;wellknown 
          mode="SingleCall" 
          type="PB.Plouffe_Bellard,Plouffe_Bellard"
          objectUri="Plouffe_Bellard"/&gt; 
      &lt;/service&gt; 
      &lt;channels&gt; 
        &lt;channel ref="tcp server" port="9000"/&gt;
      &lt;/channels&gt; 
    &lt;/application&gt; 
  &lt;/system.runtime.remoting&gt;
&lt;/configuration&gt; 
</code>
</paragraph>

<paragraph name="SimpleClient">
我创建了一个名为SimpleClient的程序，以说明客户端程序访问远程计算机上的服务器对象是多么容易。要运行SimpleClient，首先在远程计算机上运行ServerLoader，然后在本地计算机上运行SimpleClient.exe程序。在Remote Machine（远程计算机）文本框中输入远程计算机的名称，然后单击Calculate（计算）按钮开始计算第一个九位π值。SimpleClient的CalculateButton_Click方法包含客户端访问远程服务器所需的所有代码（参见代码段2）。可以使用由远程计算机名、协议(TCP)和端口号(9000)组成的URL访问远程服务器。例如，要访问我的“Pentium 200”计算机，则URL为“tcp://Pentium 200:9000/ServerLoader/Plouffe_Bellard”。创建URL后，将使用服务器的类型(Plouffe_Bellard)和URL调用Activator.GetObject。然后，返回的值被转换为Plouffe_Bellard对象以备使用。调用其CalculatePiDigits方法时，请求被发送到远程计算机上的ServerLoader。然后，服务器对象计算小数位。最后，在一个文本框中显示返回客户端程序的结果。代码段2：用于访问远程服务器的SimpleClient代码
<code>
private void CalculateButton_Click(object sender,System.EventArgs e)
{
  Cursor.Current = Cursors.WaitCursor;
  Plouffe_Bellard PiCalculator = null;
  String MachineName = RemoteMachineTextBox.Text;
  try
  {
    int port = 9000;
    String URL = "tcp://" + MachineName + ":" + 
       port + "/ServerLoader/Plouffe_Bellard";
    PiCalculator = (Plouffe_Bellard) 
       Activator.GetObject(typeof(Plouffe_Bellard), URL);
    ResultsTextBox.Text = "3." + 
       PiCalculator.CalculatePiDigits(1);
  }
  catch(Exception)
  {
    MessageBox.Show(
       "需要在计算机" +
       MachineName,
       "Simple Client上运行ServerLoader.exe", 
       MessageBoxButtons.OK,
       MessageBoxIcon.Error);
  }
  Cursor.Current = Cursors.Arrow;
}
</code>
</paragraph>

<paragraph name="Digits of Pi客户端">
Digits of Pi客户端程序比SimpleClient更复杂。SimpleClient仅通过访问远程计算机上的服务器对象来计算前九位π值。而Digits of Pi则同时使用Configure（配置）对话框中指定的远程计算机和本地计算机（如图1所示）并行计算用户指定的小数位。服务器对象在单独的线程中访问，以便在可能需要很长时间的计算过程中保持Digits of Pi GUI对用户操作的响应性。
</paragraph>

<paragraph>
Digits of Pi使用数组将作业分为九位数据块，将工作量分配到所有可用的计算机上。用户单击Calculate（计算）按钮后，将创建SolutionArray（参见图4）。SolutionArray为要计算的每组九位π值分配一个SolutionItem元素。服务器对象计算m_Digit字段指定的九位数组后，数位将存储在m_Results成员中。m_MachineName成员包含运行服务器的计算机的名称。存储计算机名是为了使Digits of Pi能够显示每台计算机计算的小数总数（参见图2）。
<image src="DotNetRemoting4.jpg" desc="图4"/>
</paragraph>

<paragraph>
为使服务器对象并行计算，Digits of Pi将为每个服务器对象创建一个线程并启动线程计算。然后，必须等待所有线程完成计算后才能显示最终结果。WaitHandle对于等待多个线程很有用。Digits of Pi将为每个线程使用一个WaitHandle，以等待所有线程完成计算。
</paragraph>

<paragraph>
将调用CalculationThread.Calculate（参见代码段3）以便为每个服务器对象创建一个线程。该操作将启动线程运行，然后返回一个AutoResetEvent（从WaitHandle衍生而来）。每个线程的AutoResetEvent都存储在一个数组中，然后数组被传递给WaitHandle.WaitAll。完成线程计算后，将对其AutoResetEvent调用Set方法。最后一个线程调用Set方法后，将返回WaitAll调用，并显示π的值。代码段3：CalculationThread。
<code>
public static WaitHandle Calculate(
SolutionArray solutionArray, String machineName)
{
  CalculationThread calculationThread = new 
    CalculationThread(solutionArray, machineName);
  Thread thread = new Thread(new 
    ThreadStart(calculationThread.Calculate));
  thread.Start();
  return calculationThread.calculationDone;
}
</code>
</paragraph>

<paragraph>
每个线程都使用相同的算法：如果有更多的工作要处理，线程将夺取下一个SolutionItem，在SolutionItem中存储服务器对象的计算机名，计算指定的九位小数，并将结果存储在SolutionItem中。此进程将一直运行，直到所有SolutionItem中都填充了结果。有关详细信息，请参见代码段4。代码段4：CalculationThread.Calculate
<code>
public void Calculate()
{
  Plouffe_Bellard PiCalculator = 
    RemotePiCalculator.GetPiCalculator(
      GetRealMachineName(machineName));
  if (PiCalculator != null)
  {
    SolutionItem Item = null;
    bool Abort;
    do
    {
      Abort = solutionArray.Abort;
      if (!Abort)
      {
        Item = solutionArray.GetNextItem();
        if (Item != null)
        {
          Item.MachineName = machineName;
          try
          {
            Item.Results = 
              PiCalculator.CalculatePiDigits(Item.Digit);
          }
          catch (Exception e)
          {
            Abort = true;
            MessageBox.Show(
              "无法访问主机上的远程对象" +
              machineName +
              Environment.NewLine + 
              Environment.NewLine +
              "Message:  " + 
              e.Message, Globals.ProgramName, 
              MessageBoxButtons.OK, 
              MessageBoxIcon.Error);
          }
          UpdateStatisticsDelegate USD = new 
            UpdateStatisticsDelegate(
              MF.UpdateStatistics);
          MF.Invoke(USD, new Object[] {} );
        }
      }
    } while (Item != null &amp;&amp; !Abort);
    calculationDone.Set();
  }
}
</code>
</paragraph>

<paragraph>
下面是每一步的说明：
<list order="true">
  <item>GetRealMachineName从多CPU计算机名中删除@1模式。例如，GetRealMachineName("Brainiac@1")返回“Brainiac”。有关多CPU计算机名的解释，请参见图1对话框中的文本。</item>
  <item>知道正确的计算机名后，将其传递给RemotePiCalculator.GetPiCalculator，这样才可以通过PiCalculator变量访问该计算机上的服务器对象。</item>
  <item>如果用户单击了Cancel（取消）按钮，将设置Abort属性。如果Abort属性为true，线程将停止计算。</item>
  <item>对MF.Invoke的调用使线程可以安全地更新ListView中的统计数据（参见图2），即使该ListView是由另一个线程创建的。在32位Windows编程中，绝不允许在创建某个控件的线程之外处理该控件。</item>
  <item>完成循环（即计算完指定的所有π位数或者用户单击Cancel [取消]按钮）后，将调用线程的AutoResetEvent的Set函数。</item>
  <item>当每个线程都调用其AutoResetEvent的Set函数后，将返回对WaitHandle.WaitAll的调用并显示结果。</item>
</list>
</paragraph>

<paragraph name="线程同步">
如果Digits of Pi的代码由多个线程同时访问，可能会有多个地方出现错误。例如，如果两个线程同时调用SolutionArray.GetNextItem，可能会返回相同的内容。这就是在GetNextItem方法中设置[MethodImpl(MethodImplOptions.Synchronized)]属性的原因，该属性可以确保一次只有一个线程调用该方法。如果方法的每一行代码都不应由多个线程同时访问，则使方法同步是一个很好的策略。
</paragraph>

<paragraph>
由于MainForm.Calculate方法只有一行代码不能同时被多个线程访问，因此它将在该行代码之前调用Monitor.Enter，并在其后调用Monitor.Exit。如果该行代码已在其他线程上运行，Monitor.Enter将被阻止。如果整个函数已实现同步，那么只保护需要防止多个线程访问的代码行就可以提高性能。
</paragraph>

<paragraph>
从System.Windows.Forms.Control派生的对象（例如Button、TextBox、RichTextBox、Label、ListBox、ListView等等）只应由创建它们的线程处理。要从非创建线程中安全处理Control衍生对象，请首先将处理代码放入一个方法，然后为该方法声明一个代理：
<code>
delegate void SetResultsTextDelegate(String Text);

private void SetResultsText(String Text)
{
  ResultsRichTextBox.Text = Text;
}
</code>
</paragraph>

<paragraph>
然后使用Form.Invoke间接调用该方法：
<code>
SetResultsTextDelegate SRTD = new 
   SetResultsTextDelegate(SetResultsText);

Invoke(SRTD, new object[] { "" } );
</code>
</paragraph>

<paragraph>
Invoke方法将从创建它的线程中调用该方法，它使用的参数与对象数组中的元素相对应。
</paragraph>

<paragraph name="小结">
.NET Remoting是一种在远程（和本地）计算机上执行代码简单有效的机制。只需将代码封装到.NET对象中，编写加载该对象并侦听请求的程序，然后在客户端程序中调用Activator.GetObject。如果您的LAN中有一些闲置的计算机，可以利用它们轻松地解决并行问题。只需记住要使用正确的线程同步机制，以防止线程之间发生冲突。
</paragraph>

</article>