内容


探索 XUL 内的多线程编程

同时从 Google、Yahoo 和 Microsoft Bing 检索搜索结果

Comments

前提条件

在本文中,您将开发一个 XUL(XML User Interface Language)应用程序。熟悉 XUL 开发会有帮助,但并非硬性要求。XUL 更像是一个 XML 小部件语言,在某些方面与 HTML 相似,也支持 JavaScript 和 CSS。您可以将其他语言与 XUL 结合使用,但在本文中我们只使用 JavaScript。因此,需要您具有 JavaScript 的经验,但对其他语言则不做要求。本文中使用的这些并发性 API 已经被引入了 Mozilla 的 JavaScript 1.9.1,它是 Firefox 3.5 的一部分。因而,您可以使用 Firefox 3.5 或 Mozilla 的 XULRunner 1.9.1 来运行本文中的代码。有关这些工具的相关链接,请参见 参考资料

问题语句

早在 2005 年,Herb Sutter 就说过免费午餐的时代已经结束了。他的分析和结论都非常简单。摩尔定律已经完成了其使命。计算机的先进不再只体现在处理器的速度上。软件开发人员也已经很好地利用了不断增长的处理速度,他们总是在不断增加所需要的计算量!这是显而易见的。计算机的运行速度加快意味着您可以做更多的事情。现在,主要通过添加内核来使计算机速度变得更快。您虽然还是可以做更多的事情,但必须使用并发编程才行。您需要使用这些内核。

也许您对此早就有所耳闻。很多编程语言和平台都已增加了新的并发性功能以帮助开发人员充分利用多内核的计算机。对于某些已经具有多线程编程 API 的语言,这就意味着通过创建更好的 API 和抽象可以使多线程编程更易懂、更少出错。一些语言本身并不支持多线程编程,对多线程编程的支持是后来加上的。XUL 程序员最主要的编程语言 JavaScript 就是这样的例子。从 JavaScript 1.9.1 开始,多线程编程就被加入了 XUL 平台上的 JavaScript。

ye olde XUL 中的线程

现在,您可能会对自已说:“等等! XUL 是 Firefox 及其扩展背后的主要技术。不要告诉我这些 XUL 应用程序一直以来都是单线程的!” 您说的当然对。但关键是要记住 XUL 支持的不只是 JavaScript。借助 XPCOM,还可以通过其他语言实现组件,特别是通过 C++。当然,C++ 能支持本机线程,所以总是可以通过该种方式在一个 XUL 应用程序中使用多线程。不过,XUL 的本机编程语言通常总是 JavaScript,或更确切地说是 Mozilla 式的 JavaScript。直到现在,JavaScript 仍然缺少一种能显式派生和管理任意线程的方法,但 JavaScript 1.9.1 却改变了这种现状。尽管只是一个较小的发布版,JavaScript 1.9.1 却增加了一项重要的新特性:通过 Workers API 实现多线程。

Workers API

那么 JavaScript 中的这个 Workers API 究竟是什么呢?有了 Workers,就可以随意派生线程,并且可以使用这些线程实现任何想要完成的任务。在很多情况下,您都会希望在应用程序中派生一个或多个线程。一种常见用例是执行一些周期性操作。这在很多应用程序中十分常见,开发人员会使用 JavaScript 的 setInterval 函数来实现这个操作。此函数的惟一缺陷是:它在主线程上执行,因此,在此期间,它会阻塞与用户的任何交互。如果它的执行占用了显著的时间,那么在执行期间应用程序就会冻结。所以,最好在一个独立的线程上执行这个函数。

这也让我想起了多线程的一个更常用的用例:长时间运行的操作。任何在主线程上运行的这类操作都会导致应用程序冻结。我们总是希望这类操作在其自已的线程上执行。那么长时间运行的操作有哪些例子呢?您可能见到过大整数质因数分解,或是对复杂数据结构的庞大列表进行分类,这些都是长时间运行的操作。但它们并不是 XUL 应用程序中的常见任务。XUL 应用程序中较为常见的是一些 IO 类的操作,如访问 Web 服务或是从本地文件系统读取。这也是我们要在本文中研究的一种操作。您将看到 Workers API 不仅允许派生线程,而且还允许在所派生的线程上任意地执行复杂的任务。最后,作为这类操作的结果,可能需要对 UI 执行某些更新,但是用另外一个线程实现这个目的是很有风险的。幸好 Workers API 在设计时考虑到了这一点,极大地简化了原本可能很困难的场景。下面让我们看看如何利用 Workers API,并在一个简单的应用程序的上下文中对它进行研究。

口味测试应用程序

如前面所提到的,多线程的一个常见应用就是某类 IO 操作,例如访问 Internet 上的一个 Web 服务。现在 JavaScript/XUL 总是可以为网络操作提供多线程:XMLHttpRequest。Ajax 的秘密武器就是在后台线程上执行网络 IO,而调用程序则提供一个 callback 函数,该函数在 IO 完成后在主线程上调用。不过,您对这个线程没有显式的控制;运行时负责这一点。此外,如果需要做多个网络操作,又该怎么办呢?在这种情况下,请求将被运行时序列化。在本应用程序中,我们将需要执行这一类任务 — 通过 Workers API 用多个线程同时访问多个远程 Web 服务。当用户查看并比较 Google、Yahoo 或 Microsoft Bing 这三个搜索引擎的匿名结果时,此应用程序将允许用户执行一个 “蒙目(blind taste)的口味测试”。

三个搜索引擎,三个线程

下面将要探讨的这个应用程序非常简单。此应用程序允许用户输入一个或多个搜索条件。随后它将搜索这三个搜索引擎,并且匿名地显示搜索结果,因此用户并不知道哪个结果列表出自于哪个搜索引擎。我们从用户界面的 XUL 代码开始(参见 清单 1)。

清单 1. XUL UI 代码
<hbox>
    <label value="Enter" id="lbl"/>
    <textbox value="" id="term"/>
    <button label="Go!" onclick="search()" id="btn"/>
</hbox>

这是一段简单的 XUL 代码,可创建一个搜索表单。它用三个小部件创建了一个水平布局。第一个小部件是一个标签,第二个小部件是用户可在其中输入搜索条件的一个文本框,第三个小部件是一个按钮。当用户单击这个按钮时,就会调用搜索函数。这也是派生 worker 线程的地方,如 清单 2 所示。

清单 2. 派生 Workers
function search(){
    var keyword = document.getElementById("term").value;
    var workerScripts = ["google.js", "yahoo.js", "bing.js"];
    var worker = {};
    for (var i=0;i<workerScripts.length;i++){
        worker = new Worker(workerScripts[i]);
        worker.onmessage = function(event) {
            displayResults(event.data);
        };
        worker.postMessage(keyword);
    }
}

如果您使用的是基于 JavaScript 1.9.1 以前版本的 XUL,那么就需要对所有这三个请求进行排队,或是使用 XPCOM 与 C++ 来有效地检索这些数据。有了 JavaScript Workers,派生多线程来检索该数据就变得十分简单。每个 Worker 对象都为其构造函数获取了一个单一字符串。这个字符串是另一个 JavaScript 文件的所在位置,该文件将成为这个 worker 的主体。worker 文件中的脚本将在其自已的线程上执行。

所谈论的这三个脚本被分别命名为 google.js、yahoo.js 和 bing.js。将它们放入一个数组,然后对该数组进行迭代。对于这个数组中的每个脚本,只需传递进指定 worker 脚本位置的那个字符串就可以创建 Worker。接下来,针对每个所创建的 worker,设置其 onmessage 函数。这个函数将在 worker 向主线程返回数据时被调用。在本例中,将此工作简单地委托给另一个名为 displayResults 的函数就可以了。最后,要发送想要用于各搜索引擎的搜索条件的名称,需要在每个 Worker 对象上调用 postMessages 函数。这将使此 worker 线程得以访问 Web 服务,进而,就可以并发调用这三个 Web 服务。不仅调用是同时的,而且结果的处理也是并行完成的。

那么这个 onmessage/postMessage 范型到底说明了什么?这是 Workers API 背后的基础线程模型。为大多数开发人员所熟知的常用线程模型是被 C++、 Java™ 技术及其他编程语言所使用的那个模型。为了通信,线程通常会修改所共享的内存。这很有效,但会带来很多问题(信号量、互斥等)。对于 UI 编程来说,也会遇到同样的问题,因为如果用多个线程更改 UI,就可能会导致 UI 锁死或崩溃。为了避免将所有这些复杂性带入 XUL,JavaScript Workers API 使用了一个更简单的模型。这个模型建立在消息传递的基础上,并且与 Erlang 和 Scala 中的 actor 模型有点类似。

让我们返回到 清单 2Worker 实例充当所派生线程的一个代理。要向线程发送一个消息,需要调用 Worker 实例上的 postMessage 方法。任何对象都可以被传递到这个方法。当然,这个派生的线程也可以将消息传递回其母线程。在向母线程传递消息时,会调用 Worker 实例的 onmessage 方法。这是一个不可选的默认实现,因此必须要覆盖它(如果您对从这个线程返回的结果感兴趣的话)。在 清单 2 中,要覆盖它,需要将它设置为等同于能接受单一参数的函数,名为 event。这是被从派生线程发送来的对象。对于 清单 2 中的这个函数,提取 event 的 data 属性,因为这是被线程发送的实际数据,然后将它发送给另一个函数来更新 UI。

您已经对母线程有了一点了解。那么对于派生或子线程您又了解了多少呢?如前面 清单 2 所示,对于每个派生线程,都要使用单独一个 JavaScript 文件。在 清单 3 中,我们先来看看访问 Google 搜索 API 的那个线程。

清单 3. Google 搜索 worker
onmessage = function(event){
    var keyword = event.data;
    var results = searchGoogle(keyword);
    postMessage(results);
}

function searchGoogle(keyword){
    var url = "http://ajax.googleapis.com/ajax/services/"+
        "search/web?v=1.0&rsz=large&q="+keyword;
    var xhr = new XMLHttpRequest();
    xhr.open("GET", this.url, false);
    xhr.send();
    var response = JSON.parse(xhr.responseText);
    var results = [];
    var result = {};
    var data = response.results;
    for (var i=0;i<data.length;i++){
        result.url = data[i].url;
        result.title = data[i].title;
        result.description = data[i].content;
        results.push(result);       
    }
    return results; 
}

清单 3 中的代码与 清单 2 中的代码有些相似。这个线程有一个 onmessage 方法。它就是每当消息被发送给线程时就会调用的函数。如果回到清单 2,那里调用的是 Worker 实例上的 postMessage 方法。这将导致清单 3 中的 onmessage 的函数的调用。这样一来,传递给 postMessage 的这个对象就成为了传递给 onmessage 方法的那个对象的数据属性。它是本示例中搜索用的关键字。

回到 清单 3,一旦从传递来的消息中抽取出了关键字,就会调用 searchGoogle 函数。此函数使用 Google 的搜索 API 来请求针对这个关键字的搜索结果。要调用此 Web 服务,请注意使用标准的 JavaScript API XMLHttpRequest。您可能已经注意到了,在用 open 方法发送这个请求时,指定了三个参数。其中最后一个参数指定了此请求是否是异步的。因它默认为 true,所以此请求是异步的。在这种情况下,需要覆盖 XMLHttpRequest 实例的 onreadystatechange 函数以便您可以为这个异步请求设置一个 callback 函数。然而,如果是从一个 Worker 线程做这样的请求,就无需这么做了。而只需将这个异步标志设置为 false,进行同步调用。这样,send 方法将被一直阻塞,直到从 Web 服务返回响应。这让您能很轻松地处理此响应。

在本例中,来自于 Google 的响应是一个 JSON 结构。您可以直接地 eval 这个响应,但使用解析器将会更安全。JavaScript 1.9.1 中也包含了来自于 json.org 的标准 JSON 解析器。因此您只需使用它的解析方法就可以安全地将 JSON 数据解析为一个 JavaScript 对象。剩下的代码则只需从 Google 返回的结构中提取数据并将这些数据放入一个公共结构。该数据随后会被通过 postMessage 函数发送回母线程。当调用 worker 线程上的 postMessage 函数时,就会用从这个 worker 线程发送来的数据调用 Worker 实例(在母线程中)上的 onmessage 方法。这就是为什么要设置 清单 2 中的这个 onmessage 函数的原因所在。其他的每个 Workers 都做着同样的工作。例如,清单 4 显示了作用于 Bing 搜索引擎的那个 worker 线程。

清单 4. Bing 搜索 worker
onmessage = function(event){
    var keyword = event.data;
    var results = searchBing(keyword);
    postMessage(results);
}

function searchBing(keyword){
    var bingAppId = "YOUR APP ID GOES HERE";
    var url = "http://api.search.live.net/json.aspx?Appid="+ 
        bingAppId+"&query="+keyword+"&sources=web";
    var xhr = new XMLHttpRequest();
    xhr.open("GET", this.url, false);
    xhr.send();
    var response = JSON.parse(xhr.responseText);
    var results = [];
    var results = {};
    var data = response.SearchResponse.Web.Results;
    for (var i=0;i<data.length;i++){
        result.url = data[i].Url;
        result.title = data[i].Title;
        result.description = data[i].Description;
        results.push(result);       
    }
    return results; 
}

如您所见,这段代码与 清单 3 中的代码十分相似。它使用 onmessage/postMessage 范型来从它的母线程接收和发送数据。当然,这个 Web 服务的 URL 有点不同。返回的结构也有点差异,但您只需将它映射回清单 3 中的公共结构即可。可以想象得到,Yahoo Worker 的代码与 Google 和 Bing 的 Worker 代码将十分相似。只是 URL 有点不一样,并且映射回常规化的数据结构的映射也有点不一样(所有源代码,参见 下载)。一旦数据进入了常规化的结构,就可以被传递给一个公共函数来更新用户界面。

结束语

无论是现在还是将来,并行编程都将是开发一个成功应用程序的关键所在。能利用多线程来从其环境收获最多的应用程序可以提供一种超级的用户体验。在本文中,您用一个非常简单的示例,查询了三个最受欢迎的搜索引擎。不过,可以想象得到同时从每个引擎检索结果为用户体验所带来的改进,而不是每次只从一个搜索引擎检索结果。即使是在一个单 CPU 的计算机上,多线程也会带来很大的不同,因为您不必花费大量时间等待 Web 服务的响应。加入到 JavaScript 1.9.1 中的 Worker API 使得为任何基于 XUL 应用程序进行这类编程变得十分容易。您可以将本文所展示的这种技术应用到 Firefox 扩展中或是使用 XULRunner 的桌面应用程序中。这些 API 很容易使用,并且您可以派生多个线程而又不必担心系统会被锁死。Workers API 为开发人员使用多线程来提高 XUL 应用程序的性能打消了所有顾虑。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Web development
ArticleID=438790
ArticleTitle=探索 XUL 内的多线程编程
publish-date=10222009