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

Legion:用 Silverlight 构建您自己的虚拟超级计算机

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (134投票s)

2007 年 12 月 25 日

LGPL3

21分钟阅读

viewsIcon

451035

downloadIcon

1081

Legion 是一个网格计算框架,它使用 Silverlight CLR 来执行用户可定义任务。它为网络客户端提供网格范围内的线程安全操作。客户端性能指标,例如带宽和处理器速度,可用于定制作业。还包括一个 WPF 管理器应用程序。

Legion logo

目录

引言

耶稣问他叫什么名字。他回答说:“我名叫‘群’,因为我们数量众多。”

马可福音 5:9 (KJV)

我一直都是分布式计算的拥趸。随着 Silverlight 的出现,我感觉这是一个激动人心的机会,可以利用客户端 Silverlight CLR 创建一个网格计算框架。Legion 就是这样应运而生的。

Legion 是一个网格计算框架,它使用 Silverlight CLR 来执行用户可定义任务。Legion 使用 ASP.NET 应用程序和 Web 服务来下载任务、上传结果数据,并为 Web 客户端或代理提供网格范围内的线程安全操作。多个任务可以同时托管,Legion 管理任务到代理的委派。客户端性能指标,例如带宽和处理器速度,可用于为客户端定制作业。Legion 提供了一个管理服务和 WPF 应用程序,用于监控 Legion 网格。

我已将 Legion 部署到此处的演示服务器,以便您可以看到它的实际运行情况。

Legion System overview.
图:Legion 系统概述。

背景

虽然网格计算并非新生事物,但由于组织希望利用其未充分利用的 IT 基础设施来执行计算密集型业务相关任务,因此对其的兴趣再次高涨。

除了大型组织纷纷效仿之外,网格计算的公众兴趣当然也受到了大型科学项目(例如 Seti@home 项目)的刺激。在对本文进行了一些初步研究后,我对志愿网格计算机能够实现的目标以及通过提供像 Legion 这样的框架可能实现的目标感到兴奋。一些志愿网格的结果令人瞩目。例如,天花研究网格项目成功地找到了 44 种有效的治疗天花的方法;这是一种迄今为止仍无法治愈的疾病。天花疫苗接种于 20 世纪 80 年代结束,但人们担心该病毒可能会再次出现。(维基百科,2007The Inquirer,2003

利用未使用的计算能力的诱惑是巨大的,许多用户贡献出原本浪费的时钟周期造福人类的想法也是如此。许多网格项目吸引了如此多参与者的一个原因是,他们感到自己属于一个共同的使命;它是免费的,用户只需在他们的计算机上在后台运行一个应用程序,就能获得为一项好事业做出贡献的感觉。

我们是否正处于网络范式转变的风口浪尖,即网络浏览器分担网络服务器的负载?这为基础设施投资很少的小型初创企业打开了大门,提供无限容量;服务任意数量的客户端。这种能力传统上一直是拥有大型数据中心的庞大公司的领域。那会带来什么呢?嗯,它将彻底改变网络;提供前所未有的民主化水平。进一步推断这个想法,想象一下,如果有一个点对点     WebTorrent     协议可能会带来什么。网络服务器和网络浏览器都充当种子并分担负载。我们有点偏离本文的范围,但我提出这些想法是为了鼓励读者产生一种机会感。因为这就是浏览器中的 CLR 现在能为我们提供的。一个机会,不仅可以创建漂亮的 UI,还可以转向一种更少服务器导向的应用程序模型。不仅仅是瘦客户端模型,而是一种分布式处理模型。

我相信安全是提供以客户端为中心的点对点志愿计算的主要挑战。保护用户隐私至关重要,发送敏感信息存在风险,因为这些信息可能被某些恶意客户端窃听或修改。同样,信任来自本质上不可信来源的结果也带来了一个问题。显然,需要机制来解决这些问题。

同样,提供安全的跨域浏览器支持将带来大量机会,使服务更容易在组织之间去中心化和共享。微软及其他公司已经提出了一些技术。然而,不可避免的是,在现有浏览器功能中实现这一点需要各种黑客手段。例如使用 IFrames 进行跨域通信。虽然阻止跨域浏览器通信似乎是合理的,但在当今网络服务普及的情况下,我们可以非常容易地将客户端数据从服务器发送到服务器,从而实现跨域,这实际上绕过了保护机制。

与其他分布式计算项目不同,Legion 允许用户通过简单地浏览网页来参与。向云计算和基于 Web 的应用程序的转变,可能加剧了用户下载和安装应用程序的日益不情愿。强制用户下载软件才能参与,虽然提高了网格的可靠性(我们稍后会讨论基于浏览器的网格的弊端),但肯定会降低总体参与率。用户更喜欢不安装软件。这很麻烦。

另一方面,Legion 允许我们通过提供诱人的内容,将新的“志愿者”带入我们的网格。因此,我们不再依赖单一的激励因素来促成参与。它为开发人员提供了一个系统,该系统将部署和执行使用任何 .NET 桌面/Silverlight CLR 兼容语言编写的模块。Legion 的优点在于它不需要用户安装任何 Legion 软件。它完全依赖用户浏览器中的 Silverlight CLR。

我听过很多 Silverlight、Flash、Java 小程序等之间的比较。大多数比较似乎都围绕着我以前的项目经理所说的“点点点”;纯 UI。好吧,在本文中,我的目标是摆脱纯粹的 UI 焦点,更多地关注在浏览器中拥有 .NET CLR 可能意味着什么。

Clog 万岁

Clog,我不知道,是这个项目的前奏,并且在它的开发中被证明是无价的。如果没有适用于 Silverlight 的 Clog,处理这个项目的并发元素将变得极其困难。可以说,我一直在“自食其果”。它使多客户端调试变得轻而易举。使用     Log4NetLogStrategy     我同时从网站、     Agents     甚至 WPF 管理器获取日志。调试多个并发执行的客户端几乎是不可能完成的任务。而且使用跟踪语句会非常糟糕。Clog 相当酷,即使是我自己说的。我已经对它的代码库进行了几次重要的更新,我将很快发布。

Legion 代理 UI

在客户端,Legion 托管在一个 Silverlight 模块中,该模块自动控制任务的下载和执行。

Legion Agent pictured in browser.
图:浏览器中显示的 Legion Agent。

提供的     Agent     可视化工具可以根据需要进行定制。我可能会在稍后提供一个更谨慎的界面。如您所见,Silverlight 模块托管在 HTML 页面上,并且乐意与其他内容共享此空间;无论是 HTML 还是其他一些交互式 Silverlight 动画,以保持用户沉迷并打开浏览器。

该界面显示了任务名称(在服务器端配置中定义)、已完成任务的数量(存储在客户端计算机的隔离存储中)以及当前正在执行任务的完成百分比。

Legion 管理器

Legion Manager 是一个 WPF 应用程序,允许监控网格。它提供了所有任务的进度摘要,以及当前的网格容量;包括兆浮点可用处理能力和所有连接客户端的总带宽。

Legion Grid Manager pictured in browser.
图:浏览器中显示的 Legion Grid Manager。

组件概述

Legion 由三个主要组件组成。GridComputingGridComputing.Management 组件使用桌面 CLR。第三个组件 GridComputing.Silverlight 是 Legion 的主要客户端组件,并在 Silverlight CLR 中执行。每个组件的主要类型如下所示。

Legion Components.
图:Legion 组件概述。

WPF 管理器是一个 WPF 应用程序,它与服务器端 GridComputing 模块通信以监视 Legion 网格。它消耗由 GridManager 创建的 GridSummary 实例。

AgentClient 实例作为管理和代理组件的每个请求的一部分传递给网格 Web 服务;它们封装了调用者的位置数据和性能指标。

MasterTaskSlaveTask 提供相应的服务器和客户端任务实现。JobMasterTask 创建,并通过 GridManager 分发给 SlaveTask

GridMonitor 是为 Agents 提供互斥的工具。GridSync 封装了代码区域的位置,GridMonitor 使用 Web 服务请求对该区域的独占访问。

Agents 使用 JsonGridService 与服务器端的 Legion 通信。当使用 Silverlight 1.1 时,我们使用 JSON JavaScript 对象表示法。您可以在我之前的一篇文章中找到更多关于使用 Silverlight 和 Web 服务的信息。

Grid Service class diagram.
图:JsonGridService 类图。

网格服务主要是 GridManager 的一个包装器,其中加入了一些额外的错误处理。以下是 JsonGridService 方法的简要概述

  • Register: 告知服务器调用 Agent 已准备好连接到网络,并可用于新任务。返回一个唯一标识符,该标识符将在将来的调用中用于标识 Agent
  • StartNewTask: 检索包含新 SlaveTask 相关信息和运行任务所需的 Job 数据的 TaskDescriptorAgent 在每个任务完成后会定期调用此方法。
  • UpdateJobProgress: 告知服务器客户端在处理 SlaveTask 中的进度。
  • JoinTask: 在 SlaveTask 在客户端完成时调用。这允许 MasterTask 处理任务结果并与其他客户端的结果合并。这是客户端 SlaveTask 的终点。
  • Disconnect: 告知服务器 Agent 将不再处理任务。
  • Download100KB: 用于测量客户端带宽。它只是返回一个大对象,将 SOAP 消息大小增加到大约 100 KB。
  • LockEnter, LockUpdate, LockExit: 用于为客户端代码块提供互斥。当 Agent 拥有独占访问权限时,它会定期使用 LockUpdate 方法进行轮询,以通知 LockManager 它仍然处于活动状态(即客户端浏览器尚未关闭)。

网格任务

创建新的 Legion 任务时,我们创建一个服务器端“主”任务和一个客户端“从”任务。自定义任务是通过扩展主 GridComputing 模块中的 MasterTask 类,以及扩展 GridComputing.Silverlight 模块中的 SlaveTask 类来创建的。MasterTask 的作用是将网格计算活动分解成小块,然后将其交给客户端 Agents。客户端 SlaveTask 是并发点;我们在此处放置我们希望由多个客户端执行的代码。当 SlaveTask 完成其工作时,结果将发送回 MasterTask,以便与其他 SlaveTask 的结果合并。

Master task class diagram.
图:MasterTask 类图。

客户端静态 TaskRunner 负责 SlaveTask 的实例化、异步执行和 Job 数据提供。TaskRunner 也是 Silverlight UI 与 Legion 之间交互的主要点。

Silverlight TaskRunner class diagram.
图:Silverlight Agent 模块 - TaskRunnerSlaveTask 类图。

为了执行客户端任务 (SlaveTask),我们需要 SlaveTaskType 名称和 Job 数据。Job 数据由 MasterTask 创建,而 TypeName 在服务器端配置中定义为 slaveType。当 Agent 调用 JsonGridService.StartNewJob(Agent agent) 时,TaskDescriptor 及其子 Job 实例将从 JsonGridService 返回。

Task Descriptor class diagram.
图:TaskDescriptorJob 类图。

配置

Legion 使用 configSection 来定义要运行的任务。首先,我们如以下摘录所示,在 configSections 元素中声明 section

<configSections>
    <section name="Grid" 
        type="Orpius.GridComputing.Configuration.GridConfigSection" 
        requirePermission="false"/>
        
    <!-- Client Logging section -->
    <section name="ClientLogging" 
        type="Orpius.Logging.Configuration.ClientLoggingConfigSection"/>
    .
    .
    .
</configSections>

然后,我们如以下摘录所示,在 section 中声明我们的网格任务。

<Grid>
    <Tasks>
        <!-- 
        slaveType: The name of the Silverlight task type.
            Please note that Version, Culture, 
            and PublicKeyToken are required.
        -->
        <add name="PrimeFinder"
             type="Orpius.GridComputing.Tasks.PrimeFinderMaster, 
                Orpius.GridComputing.Tasks.PrimeFinder"
             slaveType="Orpius.GridComputing.Tasks.PrimeFinderSlave, 
                Orpius.GridComputing.Tasks.PrimeFinder.Silverlight, 
                Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
        
        <add name="MutexExample" 
             type="Orpius.GridComputing.Tasks.MutexExampleMaster, 
                Orpius.GridComputing.Tasks.MutexExample"
             slaveType="Orpius.GridComputing.Tasks.MutexExampleSlave, 
                Orpius.GridComputing.Tasks.MutexExample.Silverlight, 
                Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
        
    </Tasks>
</Grid>

值得注意的是,每个任务的 slaveType 属性必须包含程序集的完整签名。如果我们未能包含任何部分,例如 VersionCulture 属性,Silverlight 将无法解析程序集的下载路径。

代理性能指标

为了测量连接带宽,我们只需调用一个 Web 服务,该服务返回一个大约 100 千字节的结果消息。我们在连接建立时执行一次此操作。

/// <summary>
/// Measures the bandwidth of the connection
/// with the server by calling a web service
/// method that returns a large message
/// of a predetermined size.
/// </summary>
/// <returns>The download rate of the client in KiloBytes.
/// </returns>
public static long MeasureBandwidthKBps()
{
    long resultDefault = 0;
    long result = resultDefault;

    JsonGridService gridService = JsonGridService.GetService();

    DateTime begun = DateTime.Now;

    try
    {
        byte[] download = gridService.Download100KB();
    }
    catch (Exception ex)
    {
        log.Debug("Unable to communicate with server.", ex);
        return resultDefault;
    }

    DateTime ended = DateTime.Now;

    TimeSpan span = ended - begun;
    if (span.TotalMilliseconds > 0)
    {
        result = 100000 / (long)span.TotalMilliseconds;
    }

    return result;
}

我使用 Fiddler 通过将其设置为“模拟调制解调器速度”来限制下载速度,以测试带宽测量。

Fiddler simulate modem speeds.
图:将 Fiddler 设置为模拟调制解调器速度。

在接下来的截图中,我们可以看到,在限制下载速率后,结果是 6 KB/s,这大致正确。这个精度对于我们的目的来说已经足够了。当然,我们只对每个客户端运行一次测试,因此我们必须将结果视为一个粗略的估计。

Bandwidth shown while debugging.
图:使用调制解调器速度模拟调试 Agent

为了测量客户端的处理器速度,我们让客户端执行一些浮点运算。

/// <summary>
/// Measures the processor speed in megaFLOPS.
/// Not particularly accurate, but it provides 
/// a rough indication of what the client can do.
/// </summary>
/// <returns>The processor speed in megaFLOPS.</returns>
public static long MeasureMFlops()
{
    double x = 0, y = 0, z = 0;
    DateTime startTime = DateTime.Now;
    /* 60 million fp operations? */
    for (int index = 0; index < 10000000; index++) 
    {
        x = 2.34 + index;
        y = 5.67 + index;
        z = (x * x) + (y * y) + index;
    }
    DateTime endTime = DateTime.Now;

    Console.WriteLine(x + y + z);

    TimeSpan span = endTime - startTime;

    return span.Ticks / 60;
}

当我们在服务器端 MasterTask 中为 Agent 准备作业时,我们能够利用性能指标来为客户端定制最合适的作业。例如,如果客户端连接速度较慢或无法处理大量操作,我们可以减小发送给它的作业大小。

Job tailor for Agent using performance metrics.
图:根据性能指标为代理定制的作业。

我们还可以利用其他各种指标和客户端信息。这些信息以 IAgent 实例的形式传递给 MasterTask.GetRunData 方法。

IClient and IAgent class diagram.
图:IClientIAgent 接口。MasterTask 能够使用客户端信息为代理定制作业。

素数查找任务

Legion 包含两个演示任务:素数查找器和互斥示例。第一个素数查找器在指定范围内搜索素数。每个 Agent 被指定一个范围,在该范围内测试每个数字的素性。完成后,客户端 SlaveTask 返回找到的素数列表,MasterTask 将结果插入到复合列表中。这非常简单。

/// <summary>
/// The client-side implementation of the Prime Finder task.
/// This task searches specified ranges for prime numbers
/// and then returns the results to the master.
/// </summary>
public class PrimeFinderSlave : SlaveTask
{
    static readonly ILog log 
        = LogManager.GetLog(typeof(PrimeFinderSlave));
    List<long> primes;

    public PrimeFinderSlave()
    {
        Run += AddNumberTaskClient_Run;
    }

    /// <summary>
    /// Handles the Run event of the AddNumberTaskClient control.
    /// This is where we do the processing for the task.
    /// </summary>
    void AddNumberTaskClient_Run(object sender, EventArgs e)
    {
        primes = new List<long>();

        long start = Descriptor.Job.Start;
        long end = Descriptor.Job.End;
        
        string infoMessage = string.Format(
            "PrimeFinderSlave starting. 
                Job Id is {0}. Range is {1} - {2}",
                Descriptor.Job.Id, start, end);
        log.Info(infoMessage);

        StepsCompleted = 0;
        StepsGoal = end - start;

        for (long i = start; i < end; i++)
        {
            if (IsPrime(i))
            {
                primes.Add(i);
            }
            StepsCompleted++;
            /* Sleep for a bit. */
            if (i % 5000 == 0)
            {
                int sleepTime = 200;
                System.Threading.Thread.Sleep(sleepTime);
            }
        }
        Result = primes;
    }

    static bool IsPrime(long candidate)
    {
        for (int d = 2; d <= candidate / 2; d++)
        {
            if (candidate % d == 0)
            {
                return false;
            }
        }
        return candidate > 1;
    }
}

一旦 MasterTask 完成向 Agents 分发搜索范围,它将继续分发 未加入Jobs,直到获得所有结果。

代理任务执行

SlaveTask 在客户端使用从服务器端 GridManager 通过网络传递的 TaskDescriptor 实例中的信息进行实例化。TaskDescriptor 包含一个由 SlaveTask 执行的 Job。客户端 TaskRunner 负责确保任务的下载和实例化。并且,正是 TaskRunner 负责监督 SlaveTask 的运行。

Fetching a job.
图:任务运行程序正在获取作业。

执行任务的过程在下一张图中更详细地概述。在这里,我们看到当 Silverlight 代理在浏览器中开始执行时,它会自动启动一个客户端任务。Agent 向 Legion 服务器注册,下载 TaskDescriptor 并开始处理作业。

Task execution flowchart.
图:任务执行。

当客户端 SlaveTask 完成后,结果由 MasterTask 合并,并且 TaskRunner 请求一个新任务。在所有 MasterTask 完成后,一个 Enabled 属性的 Job 将传递回 Agent,表明没有剩余的工作要做。此时 Agent 将处于空闲状态。但它将继续定期向服务器请求任务,以防有新任务可用。

客户端互斥

为了让 SlaveTask 能够安全地使用共享数据,我们需要一种互斥机制。正如 .NET 提供了各种类型,包括用于线程并发的 Monitor 类,我创建了 GridMonitor 类。通过 GridMonitor,我们能够异步安全地访问代码区域。GridMonitor 类使用 JsonGridService 来控制对使用 GridSync 实例标识的区域的访问。每个 GridSync 标识一个或多个代码区域,这些区域必须一次只允许一个客户端访问。

Silverlight GridMonitor class diagram.
图:Silverlight Agent 模块,GridMonitor 类图。

Agent 希望获得代码块的独占访问权限时,它必须使用 GridMonitor 结合 GridSync 向服务器端 LockManager 发出请求。GridSync 的目的是标识要同步的一个或多个代码区域。GridSyncScopeTypeName 属性构成了 GridSync 标识的一部分,通常是实例化 GridSync 的类 typeLocalName 构成了标识的第二部分,并且是相对于 ScopeTypeName 的名称。ScopeTypeNameLocalName 的组合通常应该是唯一的,否则可能会发生死锁。它类似于使用 lock (objectInstance) {...} 语法,并以相同的方式行为。一旦 Client 拥有锁,它就可以从任何它喜欢的线程执行代码块。因此,在客户端使用 GridMonitor 进行多线程处理时仍需谨慎,并应采用 FCL 中使用 Monitors 等标准互斥技术。

Agent denied ownership of lock.
图:代理被拒绝拥有锁。GridMonitor 将阻塞,直到授予所有权或发生超时。

当使用 GridMonitor 时,我们将其封装在 using 语句中。当执行线程离开 using 块时,GridMonitor 将被释放,并尝试通过调用其自身的 Exit 方法来释放锁。这将向服务器端 LockManager 发出异步调用;自动放弃锁的所有权。

GridMonitor 拥有锁时,它将定期轮询服务器端 LockManager,告知它 Agent 仍然存活,并且锁的所有权应仍归该 Agent 所有。

Lock Manager class diagram.
图:LockManager 类图。

以下摘录自 MutexExampleSlave 任务,演示了如何使用 GridMonitor 创建互斥块。

/// <summary>
/// This is the client side implementation for a task
/// that demonstrates the use of the <see cref="GridMonitor"/>.
/// </summary>
public class MutexExampleSlave : SlaveTask
{
    static readonly ILog log 
        = LogManager.GetLog(typeof(MutexExampleSlave));
    const int iterationsCount = 10;

    GridSync exampleSync;
    
    public MutexExampleSlave()
    {
        Run += MutexExampleTask_Run;
    }

    void MutexExampleTask_Run(object sender, EventArgs e)
    {
        long id = Descriptor.Job.Id;

        string infoMessage = string.Format(
            "MutexExampleSlave starting. Job Id is {0}.", id);
        log.Info(infoMessage);

        /* Get the client id for logging purposes. */
        Guid clientId = new Guid(Descriptor.Job.CustomData.ToString());
        /* The GridSync dispatches calls to the web service,
         * and then on to the GridManager. */
        exampleSync = new GridSync(typeof(MutexExampleSlave), "test");

        StepsGoal = iterationsCount * 10;

        for (int i = 0; i < iterationsCount; i++)
        {
            /* Here we attempt to enter the monitor. 
             * This is done automatically when 
             * we instantiate a GridMonitor. 
             * The Monitor will Exit when it is disposed. 
             * We may block here for a while, 
             * and wait for another agent 
             * to leave the using statement. */
            using (new GridMonitor(exampleSync))
            {
                log.Info(string.Format(
                    "Locked id:{0} iteration:{1} cliendId: {2}", 
                    id, i, clientId));
                    
                for (int j = 1; j <= 10; j++)
                {
                    System.Threading.Thread.Sleep(200);
                    StepsCompleted = i * 10 + j;
                }
            }
            log.Info(string.Format(
                "Unlocked id:{0}, and now sleeping for 2 seconds.", id));
            System.Threading.Thread.Sleep(2000);
        }

        /* Once we leave this method, the Complete event will be raised. */
    }
}

以下摘录自 GridMonitor 类,演示了当它被释放时,如何调用 Grid Web 服务来放弃其锁。

protected virtual void Dispose(bool IsDisposing)
{
    if (disposed)
    {
        return;
    }

    if (IsDisposing)
    {
        /* Allow other agents to enter using statement. */
        ReleaseLock();
    }

    /* Free any unmanaged resources 
     * in this section. */
    disposed = true;
}

MutexExampleMaster 任务除了递增任务计数器和为演示目的在客户端分发 ID 之外,几乎没有做任何事情。代码在此提供,以供参考。

/// <summary>
/// This is the server side implementation for a task
/// that demonstrates the use of the GridMonitor.
/// </summary>
class MutexExampleMaster : MasterTask
{
    long taskCounter;
    const int runTimes = 100;

    public MutexExampleMaster()
    {
        StepsGoal = runTimes;
    }

    /// <summary>
    /// Gets the run data for the <see cref="Agent"/> slave task.
    /// This data should encapsulate the task segment
    /// that will be worked on by the slave. <seealso cref="Job"/>
    /// </summary>
    /// <param name="agent">The agent requesting the run data.</param>
    /// <returns>The job for the agent to work on.</returns>
    public override Job GetRunData(IAgent agent)
    {
        Job job = new Job(++taskCounter);
        job.CustomData = agent.Id;

        if (taskCounter >= runTimes)
        {
            throw new InvalidOperationException("Task is complete.");
        }
        StepsCompleted = taskCounter;

        return job;
    }

    /// <summary>
    /// Joins the specified task result. This is called
    /// when a slave task completes its <see cref="Job"/>,
    /// after having called <see cref="GetRunData"/>;
    /// returning the results to be integrated
    /// by the associated <see cref="MasterTask"/>.
    /// </summary>
    /// <param name="taskResult">The task result.</param>
    public override void Join(TaskResult taskResult)
    {
        if (taskCounter > 100)
        {
            Completed = true;
        }
    }
}

过期集合

创建使用 Web 客户端的志愿网格计算平台最困难的挑战是网络的易变性和客户端的瞬态性质。我们永远无法确定一个节点是否仍然连接。当客户端连接时,我们为其分配一个“连接窗口”。这是一个时间跨度,在此期间客户端必须调用主服务器才能被视为仍然存活。如果我们正在进行并发活动,并且客户端在此窗口内没有调用主服务器,那么我们必须重新获得客户端拥有的任何锁的所有权。否则可能会发生死锁。此外,如果我们不释放与连接相关的数据,那么我们将发生内存泄漏。因此,如果 Agents 未能在此窗口内调用主服务器,我们会定期使其超时。为了实现这一点,我选择实现一个自包含的机制来使客户端数据过期。这以两个“过期集合”的形式出现;一个 ExpiringDictionary 和一个 ExpiringQueue。结果证明,它们非常有用。

Expiring Collections class diagram
图:ExpiringDictionaryExpiringQueue 集合。

两者与 System.Collections.Generic 命名空间中的同类集合工作方式相同,但增加了计时器和“触碰”项目以更新其插入日期戳的功能。两者都使用内部计时器将其内部集合中的项目弹出。

Expiring Dictionary
图:ExpiringDictionary。项目由计时器定期移除。

ExpiringDictionaryGridManager 用于跟踪已连接的 Agents 及其 Jobs 的进度,也被 LockManager 用于将锁与各自的 Agent 关联。ExpiringQueue 也被 LockManager 使用,它维护着一个由 LockSyncExpiringQueue 对组成的 ExpiringDictionary。我们将 LockSync 与一个 Agent 队列关联起来;从而定期使针对某个代码块的所有请求过期,并定期使针对代码块的单个请求过期。这最大限度地减少了我们需要维护的锁请求信息量。例如,如果 MasterTask 完成,那么我们可以忘记所有与其 SlaveTask 相关的锁。或者如果用户关闭了他们的浏览器,在调用 GridMonitor.Enter 之后,我们可以在指定时间后安全地删除请求并释放锁。

SlaveTasks 在客户端以低优先级线程执行,并由 TaskRunner 监督。

使用隔离存储保存和检索数据

Legion 使用隔离存储来存储用户已处理的任务数量。隔离存储是特定于每个用户的虚拟文件系统。因此,对于同一用户的多个并发浏览器,在同一域内,我们能够共享数据。我提供了一个静态 IsolatedStorageUtility 类来帮助存储和检索应用程序设置和数据。不幸的是,我无法提供我所希望的功能,因为没有一个 Safe Critical 设施来将对象序列化为二进制、XML 或 JSON。一旦我们可以,也许在 Silverlight 2.0 中,我将回来处理它。

Silverlight IsolatedStorage class diagram.
图:Silverlight Agent 模块,IsolatedStorage 类图。

使用隔离存储保存数据

using (IsolatedStorageFile storageFile
        = IsolatedStorageFile.GetUserStoreForApplication())
{
    if (value == null || value.ToString().Trim().Length == 0)
    {
        storageFile.DeleteFile(fileName);
    }
    else
    {
        using (IsolatedStorageFileStream isoStream
            = new IsolatedStorageFileStream(
                fileName, FileMode.Create, storageFile))
        {
            using (StreamWriter writer = new StreamWriter(isoStream))
            {
                writer.Write(value);
            }
        }
    }
}

使用隔离存储检索数据

using (IsolatedStorageFile storageFile
    = IsolatedStorageFile.GetUserStoreForApplication())
{
    using (IsolatedStorageFileStream isoStream
        = new IsolatedStorageFileStream(
            fileName, FileMode.Open, storageFile))
    {
        using (StreamReader reader = new StreamReader(isoStream))
        {
            /* Read the to the end of the file. */
            String storedValue = reader.ReadToEnd();
            return storedValue;
        }
    }
}

如何编写网格任务

我们已经了解了 Legion 的内部工作原理,但您可能更感兴趣的是如何创建自己的自定义任务。我们只需扩展两个类,然后将任务添加到配置中。此版本随附的两个示例任务将为您提供必需品。

首先,创建一个新的常规 .NET 类库项目,并添加对主 GridComputing 模块的引用。在该项目中,创建一个扩展 MasterTask 的新类,并提供 GetJobJoin 方法的覆盖。第一个方法 GetJob 负责为客户端 Agent 提供运行任务所需的一切。Join 方法接收来自 SlaveTask 的输出,并将其与其他 SlaveTask 的输出结合起来。

接下来,创建一个新的 Silverlight 类库项目,并添加对 SilverlightAgent 项目的引用。这将包含任务的客户端代码。创建一个新类并扩展 SlaveTask。为 slave 项目包含一个 Post Build Event,它将把构建的程序集复制到您网站的 ClientBin 目录。无需从 SilverlightAgent 项目引用 Silverlight 从属任务项目。实际上,我建议您不要这样做。它们只需复制到网站的 ClientBin 目录,Silverlight 将负责将程序集下载到浏览器。

Post build event to copy output assembly.
图:构建后事件将从属程序集复制到网站 Silverlight 客户端 bin。

最后,在网站的 web.config 中为 Task 定义一个新的配置元素。这应包含 SlaveTask 的完全限定的 type 名称。

未来增强功能

  • Agents 提供统一的共享存储设施。
  • 实现半双工或全双工信道。

结论

基于 Web 的志愿网格计算为利用网站访问者的闲置资源提供了巨大的机会。我们能够以虚拟超级计算机的形式利用这一资源,它使我们能够以传统客户端-服务器模型之外的方式提供服务。本文讨论了 Legion 的设计和使用,Legion 是一个使用 Silverlight CLR 执行用户可定义任务的网格计算框架。我们看到了 Legion 如何使用 ASP.NET 应用程序和 Web 服务来下载任务、上传结果数据,并为代理提供网格范围内的线程安全操作。我们还研究了如何使用客户端性能指标(例如带宽和处理器速度)来调整 Agents 的工作负载。我希望本文能进一步激发人们对网格计算的兴趣,并提高对 Silverlight 不仅仅是 UI 工具的认识。

我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。

特别感谢

感谢 decav.comLAB 49 的 Andre de Cavaignac 慷慨允许使用他出色的 WPF 折线图。

参考文献

历史

2007 年 12 月

  • 首次发布。

2008 年 10 月 26 日

  • 支持 Silverlight 2 RTM。
© . All rights reserved.