.NET Remoting 使您可以跨多台计算机轻松进行分布计算,只需完成非常少的编程工作。在本文中,Eric Bergman-Terrell 创建了一个名为 Digits of Pi 的应用程序,它使用并行的多台计算机以不可思议的精度计算 p 值。他设法在 12 小时内完成了 10,000 位数的计算,却只使用了相当少的计算资源。这比用一台计算机单独完成计算快了 300%。
单击下载文件下载示例应用程序源代码后,打开 Everything.sln 解决方案。此解决方案包含运行“Digits of Pi”应用程序所需的三个项目(Client、Server 和 ServerLoader)。还包含一个名为 SimpleClient 的项目,我们稍后再讨论它。加载 Everything.sln 之后,请选择 Build(编译)| Batch Build...(批编译...)。单击 Select All(全部选定)按钮,然后单击 Build(编译)。编译所有内容后,请在本地计算机以及您的 LAN 中的远程计算机上安装该软件。
在本地计算机上,创建一个文件夹并将以下文件复制到其中:
Server/bin/Release/Plouffe_Bellard.dll Client/bin/Release/DigitsOfPi.exe在每个远程计算机和本地计算机上,创建一个文件夹并将以下文件复制到其中:
Server/bin/Release/Plouffe_Bellard.dll ServerLoader/bin/Release/ServerLoader.exe ServerLoader/ServerLoader.exe.config然后运行 ServerLoader.exe 程序。当然,运行 ServerLoader 和 Digits of Pi 程序之前,需要在每台计算机上安装 .NET Framework。
在所有远程计算机和本地计算机上运行 ServerLoader 程序后,请运行 Digits of Pi 程序。单击 Configure...(配置...)(参见图 1),添加本地计算机名和远程计算机名。如果不确定某台计算机的名称,请查看 ServerLoader 程序,它在表中显示其计算机名。如果您很幸运地拥有一个多 CPU 系统,您只需为所有 CPU 输入一次计算机名。只需在计算机名后键入 @ 符号和一个编号。例如,如果您拥有一个名为“Brainiac”的双 CPU 系统,则键入以下计算机名:“Brainiac@1”和“Brainiac@2”。不必为多个 CPU 系统输入多个计算机名,但是这样做可以确保所有计算机的 CPU 都用于计算 p 值。输入所有计算机名后,单击 OK(确定)。
然后指定要计算的位数(参见图 2)并单击 Calculate(计算)。请从较少的位数开始,p 值小数点后面的位数越多,程序所需的时间就越长。
图 3 显示了 Digits of Pi 程序如何在本地计算机和远程计算机中分配工作量,它使用 TCP/IP 端口 9000 发送请求并接收结果。接下来,我们将详细探讨 Remoting、Plouffe_Bellard 服务器对象、ServerLoader 程序、SimpleClient 程序和 Digits of Pi 程序。
您可以通过以下步骤使用 .NET Remoting 访问远程对象:
创建从 System.MarshalByRefObject 继承的 .NET 服务器对象 (DLL)。该服务器对象将在远程计算机和本地计算机上运行。 创建通过调用 RemotingConfiguration.Configure 加载服务器对象的服务器加载器程序。服务器加载器程序也将在远程计算机和本地计算机上运行。 创建使用 Activator.GetObject 访问服务器对象的客户端程序。您需要添加对服务器对象的引用以编译此程序。此客户端程序只在本地计算机上运行。
表 1:ServerLoader.exe.config。
<configuration> <system.runtime.remoting> <application name = "ServerLoader"> <service> <wellknown mode="SingleCall" type="PB.Plouffe_Bellard,Plouffe_Bellard" objectUri="Plouffe_Bellard"/> </service> <channels> <channel ref="tcp server" port="9000"/> </channels> </application> </system.runtime.remoting> </configuration>
表 2:用于访问远程服务器的 SimpleClient 代码。
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; }
Digits of Pi 使用数组将作业分为九位数据块,将工作量分配到所有可用的计算机上。用户单击 Calculate(计算)按钮后,将创建 SolutionArray(参见图 4)。SolutionArray 为要计算的每组九位 p 值分配一个 SolutionItem 元素。服务器对象计算 m_Digit 字段指定的九位数组后,数位将存储在 m_Results 成员中。m_MachineName 成员包含运行服务器的计算机的名称。存储计算机名是为了使 Digits of Pi 能够显示每台计算机计算的小数总数(参见图 2)。
为使服务器对象并行计算,Digits of Pi 将为每个服务器对象创建一个线程并启动线程计算。然后,必须等待所有线程完成计算后才能显示最终结果。WaitHandle 对于等待多个线程很有用。Digits of Pi 将为每个线程使用一个 WaitHandle,以等待所有线程完成计算。
将调用 CalculationThread.Calculate(参见表 3)以便为每个服务器对象创建一个线程。该操作将启动线程运行,然后返回一个 AutoResetEvent(从 WaitHandle 衍生而来)。每个线程的 AutoResetEvent 都存储在一个数组中,然后数组被传递给 WaitHandle.WaitAll。完成线程计算后,将对其 AutoResetEvent 调用 Set 方法。最后一个线程调用 Set 方法后,将返回 WaitAll 调用,并显示 p 的值。
表 3:CalculationThread。
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; }每个线程都使用相同的算法:如果有更多的工作要处理,线程将夺取下一个 SolutionItem,在 SolutionItem 中存储服务器对象的计算机名,计算指定的九位小数,并将结果存储在 SolutionItem 中。此进程将一直运行,直到所有 SolutionItem 中都填充了结果。有关详细信息,请参见表 4。
表 4:CalculationThread.Calculate。
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 && !Abort); calculationDone.Set(); } }下面是逐步的说明:
GetRealMachineName 从多 CPU 计算机名中删除 @1 模式。例如,GetRealMachineName("Brainiac@1") 返回 "Brainiac"。有关多 CPU 计算机名的解释,请参见图 1 对话框中的文本。 知道正确的计算机名后,将其传递给 RemotePiCalculator.GetPiCalculator,这样才可以通过 PiCalculator 变量访问该计算机上的服务器对象。 如果用户单击了 Cancel(取消)按钮,将设置 Abort 属性。如果 Abort 属性为 true,线程将停止计算。 对 MF.Invoke 的调用使线程可以安全地更新 ListView 中的统计数据(参见图 2),即使该 ListView 是由另一个线程创建的。在 32 位 Windows 编程中,绝不允许在创建某个控件的线程之外处理该控件。 完成循环(即计算完指定的所有 p 位数或者用户单击 Cancel [取消] 按钮)后,将调用线程的 AutoResetEvent 的 Set 函数。 当每个线程都调用其 AutoResetEvent 的 Set 函数后,将返回对 WaitHandle.WaitAll 的调用并显示结果。
由于 MainForm.Calculate 方法只有一行代码不能同时被多个线程访问,因此它将在该行代码之前调用 Monitor.Enter,并在其后调用 Monitor.Exit。如果该行代码已在其他线程上运行,Monitor.Enter 将被阻止。如果整个函数已实现同步,那么只保护需要防止多个线程访问的代码行可以提高性能。
从 System.Windows.Forms.Control 衍生的对象(例如 Button、TextBoxe、RichTextBoxe、Label、ListBoxe、ListView 等等)只应由创建它们的线程处理。要从非创建线程中安全处理 Control 衍生对象,请首先将处理代码放入一个方法,然后为该方法声明一个代理:
delegate void SetResultsTextDelegate(String Text); private void SetResultsText(String Text) { ResultsRichTextBox.Text = Text; }然后使用 Form.Invoke 间接调用该方法:
SetResultsTextDelegate SRTD = new SetResultsTextDelegate(SetResultsText); Invoke(SRTD, new object[] { "" } );Invoke 方法将从创建它的线程中调用该方法,它使用的参数与对象数组中的元素相对应。
下载 TERRELL.ZIP