.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 访问远程对象:
表 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();
}
}
下面是逐步的说明:
由于 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