多线程编程和单线程编程
尽管越来越多的网站完全或部分基于AJAX,但是开发复杂的AJAX应用程序仍然很困难。 导致开发AJAX应用程序困难的主要问题是什么? 它是与服务器的异步通信,还是GUI编程? 两者通常由桌面窗口应用程序执行-那么为什么开发具有相同功能的AJAX应用程序特别困难?
AJAX应用程序开发困难
让我们用一个简单的例子来考虑这个问题。 假设您要构建一个树状的布告栏系统,该系统通过根据用户请求与服务器通信而不是一次从服务器加载所有文章来为每个文章加载数据。 每个文章都有4条与之相关的信息:布告栏系统中的唯一ID,发布文章的人员的姓名,文章的内容以及其子文章的ID数组。 首先,我们假设有一个名为getArticle()
JavaScript函数,该函数负责加载单个文章。 该函数接收要加载的商品的整数ID作为参数,并从服务器检索具有该ID的商品数据。 然后,它返回一个对象,该对象包含该文章中包含的4条信息: id, name, content
和children
。 可以使用以下形式编写此函数的示例:
function ( id ) {
var a = getArticle(id);
document.writeln(a.name + "<br>" + a.content);
}
您可能会注意到,以相同的商品ID多次调用此函数需要无缘无故地与服务器进行一次通信。 为了解决此问题,请考虑函数getArticleWithCache()
,它是具有缓存功能的getArticle()
。 在此示例中,由getArticle()
加载的数据将简单地保留为全局变量:
var cache = {};
function getArticleWithCache ( id ) {
if ( !cache[id] ) {
cache[id] = getArticle(id);
}
return cache[id];
}
现在,已阅读的文章将被缓存。 现在,让我们考虑一下函数backgroundLoad()
,该函数基于此机制加载所有文章的数据。 该功能旨在在用户阅读给定文章时将所有子文章预加载到后台。 由于商品数据是树状结构的,因此可以轻松编写遍历树并允许加载所有商品的递归算法:
function backgroundLoad ( ids ) {
for ( var i=0; i < ids.length; i++ ) {
var a = getArticleWithCache(ids[i]);
backgroundLoad(a.children);
}
}
backgroundLoad()
函数接收ID数组作为参数,并将我们先前定义的getArticleWithCache()
应用于每个ID。 这允许与每个ID相对应的商品数据被缓存。 然后,通过对加载的文章的子文章的ID递归调用backgroundLoad()
,将缓存整个文章树。
到目前为止,一切看起来都不错。 但是,如果您从事AJAX应用程序开发工作,那么您应该知道,这种幼稚的实现将无法成功进行。 该示例基于对getArticle()
使用同步通信的默认了解。 但是,作为一般规则,JavaScript与服务器进行通信时需要使用异步通信,因为它只有一个线程。 简单起见,在一个线程上处理所有内容(包括GUI事件和渲染)是一个很好的编程模型,因为它无需考虑与线程同步相关的复杂问题。 另一方面,它在开发应用程序时提出了一个重大问题–看起来对用户有响应,因为单线程环境无法在线程正在处理其他事情(例如线程)时响应用户的鼠标单击和/或键操作。 getArticle()
调用)。
如果在此单线程环境中执行同步通信会怎样? 同步通信将停止浏览器的执行,直到获得通信结果为止。 在等待通信结果时,线程无法响应用户,因为来自服务器的调用尚未完成,线程将保持阻塞状态,直到调用返回。 因此,它在等待服务器响应时无法响应用户,因此浏览器看起来冻结了。 这也为执行成立getArticleWithCache()
和backgroundLoad()
其基于getArticle()
由于可能花费大量时间来下载所有文章,因此这段时间内浏览器的冻结对于backgroundLoad()
是一个严重的问题-由于浏览器被冻结,因此不可能首先实现预加载的目标用户正在阅读文章时,后台中的数据,因为文章将不可读。
如上所述,由于使用同步通信会在可用性方面造成重大问题,因此JavaScript将异步通信用作一般规则。 因此,让我们基于异步通信重写上面的程序。 JavaScript需要以事件驱动的编程风格编写异步通信。 在大多数情况下,您可以指定一个回调函数,该函数在收到通信响应后即被调用。 例如,上面定义的getArticleWithCache()
可以重写为:
var cache = {};
function getArticleWithCache ( id, callback ) {
if ( !cache[id] ) {
callback(cache[id]);
} else {
getArticle(id, function( a ){
cache[id] = a;
callback(a);
});
}
}
该程序还内部调用getArticle()
函数。 但是,应该注意,设计用于异步通信的getArticle()
版本期望接收一个函数作为第二个参数。 调用此版本的getArticle()
,它将像以前一样向服务器发送请求,但是该函数将立即返回,而无需等待服务器的响应。 这意味着当执行返回给调用方时,尚未检索到服务器响应。 这使线程可以执行其他任务,直到获得服务器响应并调用回调函数为止。 从服务器收到此响应后,将以服务器的响应作为参数来调用指定为getArticle()
的第二个参数的回调函数。 同样, getArticleWithCache()
已更改,因此它将期望将回调函数作为第二个参数。 然后,将在传递给getArticle()
的回调函数中调用此回调函数,以便在服务器通信完成后执行该回调函数。
您可能会认为上面的重写非常复杂,但是backgroundLoad()
函数涉及更复杂的重写。 也可以重写它以处理回调函数:
function backgroundLoad ( ids, callback ) {
var i = 0;
function l ( ) {
if ( i < ids.length ) {
getArticleWithCache(ids[i++], function( a ){
backgroundLoad(a.children, l);
});
} else {
callback();
}
}
l();
}
重写后的backgroundLoad()
函数看起来与我们的原始函数不太相似,但是它们的作用没有什么不同。 这意味着两个函数都接收一个ID数组,在该数组的每个元素上调用getArticleWithCache()
,然后将backgroundLoad()
递归地应用于结果子项。 但是,要识别数组的循环结构也不容易,这在原始程序中用for语句表示。 为什么这两组执行相同功能的功能彼此完全不同?
产生这种差异的原因是,任何函数都必须在需要服务器通信的任何函数getArticleWithCache()
例如getArticleWithCache()
之后立即返回。 除非原始函数不再执行,否则无法调用应接收服务器响应的回调函数。 对于JavaScript,不可能在循环中间(例如for语句)挂起程序,并在挂起执行点的稍后继续执行; 因此,循环是通过递归传递回调函数而不是使用循环语法来表示的。 对于那些熟悉连续传递样式(CPS)的人,这是CPS的手动实现。 因为不能使用循环语法,所以即使前面描述的遍历树的简单程序也需要复杂的语句。 与事件驱动程序相关的问题被称为控制流问题 :循环和其他控制流语句可能很难理解。
还有另一个问题:如果将不使用异步通信的函数转换为使用异步通信的函数,则重写的函数将需要有一个新参数,即回调函数。 这给现有的API带来了严重的问题,因为我们的内部更改将不会保留在内部,而是会导致API损坏以及其他使用我们的API的更改。
所有这些问题的根本原因是什么? 那就对了。 JavaScript只有一个线程这一事实导致了问题。 仅在一个线程上执行异步通信需要事件驱动程序和复杂的语句。 如果在程序等待服务器响应时另一个线程可以响应用户,则不需要这样的杂技。
邀请参加多线程编程
让我说说Concurrent.Thread,它是一个允许JavaScript使用多个线程的库,因为这大大减轻了上述AJAX开发中与异步通信相关的困难。 这是一个用JavaScript实现的免费软件库,可以在Mozilla Public License / GNU General Public License下获得。 您可以从网站下载源代码。
让我们立即下载并使用源代码。 假设您已将下载的源代码保存为名为Concurrent.Thread.js的文件。 在做其他事情之前,让我们运行下面的程序,该程序具有非常简单的实现:
<script type="text/javascript" src="Concurrent.Thread.js"></script>
<script type="text/javascript">
Concurrent.Thread.create(function(){
var i = 0;
while ( 1 ) {
document.body.innerHTML += i++ + "<br>";
}
});
</script>
执行此程序应按顺序显示以0开头的数字。 数字依次显示,您可以通过滚动页面来查看。 现在,让我们更详细地查看源代码。 它使用了一个简单的无限循环,如while ( 1 )
。 在通常情况下,像这样JavaScript程序会继续使用一个线程,并且仅使用一个线程,从而导致浏览器看上去死机。 自然,它不允许您滚动屏幕。 然后,为什么上面的程序允许您滚动? 关键是while ( 1 )
上方的Concurrent.Thread.create()
( 1 )
。 这是库提供的一种方法。 它用于创建新线程。 在新线程上,执行作为参数传递的函数。 让我稍微重写一下程序,如下所示:
<script type="text/javascript" src="Concurrent.Thread.js"></script>
<script type="text/javascript">
function f ( i ){
while ( 1 ) {
document.body.innerHTML += i++ + "<br>";
}
}
Concurrent.Thread.create(f, 0);
Concurrent.Thread.create(f, 100000);
</script>
在此程序中,我们有一个新函数f()
,该函数反复显示数字。 这是在顶部定义的,并且以f()
作为参数调用了create()
方法两次。 传递给create()
方法的第二个参数无需修改即可传递给f()
。 执行此程序将显示一些小数字(从0开始),然后是一些大数字(从100,000开始),再有小数字又跟随第一个小数字系列。 这样,您可以观察到程序显示了小数和大数的交替行。 这表明两个线程正在同时运行。
让我向您展示Concurrent.Thread的另一种用法。 在上面的示例中,调用create()
方法创建线程。 也可以创建线程而根本不调用任何库API。 例如,前一个示例可以表示为:
<script type="text/javascript" src="Concurrent.Thread.js"></script>
<script type="text/x-script.multithreaded-js">
var i = 1;
while ( 1 ) {
document.body.innerHTML += i++ + "<br>";
}
</script>
在script标签内,无限循环是用JavaScript简单编写的。 您应该注意标记的type属性:一个不熟悉的值( text/x-script.multithreaded-js
)被分配给它。 如果将此属性分配给脚本标签,则Concurrent.Thread在新线程上执行标签的内容。 您还应该记住,在这种情况下,还必须包含Concurrent.Thread的库主体。
使用Concurrent.Thread,即使您编写一个长而连续的程序,也可以根据需要将执行上下文从一个线程切换到另一个线程。 让我简要地谈谈这种行为是如何实现的。 简而言之,使用代码转换。 粗略地说,传递给create()
方法的函数首先转换为字符串,然后将其重写,以便可以逐个执行。 然后,重写的功能在调度程序上一点一点地执行。 调度程序负责协调多个线程。 换句话说,它会进行调整,以使每个重写的功能都能平均执行。 Concurrent.Thread实际上不会创建新线程,而只是在原始单线程上模拟多线程环境。
尽管转换后的函数似乎在不同的线程上运行,但实际上只有一个线程在运行所有线程。 在转换后的函数中执行同步通信仍将导致浏览器冻结。 您可能会认为我们原来的问题根本没有解决,但是您不必担心。 Concurrent.Thread提供了一个专用的通信库,该库使用异步JavaScript通信样式实现,并且旨在允许其他线程即使在线程正在等待服务器响应时也可以工作。 该通信库位于Concurrent.Thread.Http
命名空间下。 例如,它的用法如下:
<script type="text/javascript" src="Concurrent.Thread.js"></script>
<script type="text/x-script.multithreaded-js">
var req = Concurrent.Thread.Http.get(url, ["Accept", "*"]);
if (req.status == 200) {
alert(req.responseText);
} else {
alert(req.statusText);
}
</script>
顾名思义, get()
方法使用HTTP GET
检索指定URL的内容。 它以目标URL作为第一个参数,并将代表HTTP标头字段的数组作为可选的第二个参数。 get()
方法与服务器通信,并在接收到服务器响应后返回XMLHttpRequest对象作为返回值。 当get()
方法返回时,已收到响应。 不必使用回调函数来接收结果。 自然,不必担心在程序等待服务器响应时浏览器会冻结。 另外, post()
方法可用于将数据发送到服务器:
<script type="text/javascript" src="Concurrent.Thread.js"></script>
<script type="text/x-script.multithreaded-js">
var req = Concurrent.Thread.Http.post(url, "key1=val1&key2=val2");
alert(req.statusText);
</script>
post() method takes a destination URL as the first argument and content body to be sent as the second argument. As with the
get()
method, you can also assign header fields by the optional third argument.post() method takes a destination URL as the first argument and content body to be sent as the second argument. As with the
get()
method, you can also assign header fields by the optional third argument.
如果在第一个示例中使用此通信库实现getArticle()
,则可以使用本文getArticle()
显示的朴素方法快速编写getArticleWithCache()
, backgroundLoad()
和其他使用getArticle()
函数。 即使那个版本的backgroundLoad()
正在读取文章数据,理所当然地,另一个线程也可以响应用户,因此浏览器不会冻结。 现在,您了解在JavaScript中使用多个线程有多有用吗?
想要查询更多的信息
我解释了Concurrent.Thread,这是一个库,可让您在JavaScript中使用多个线程。 本文中的说明仅是介绍。 如果您想了解更多信息,建议您阅读本教程 。 本教程提供了有关Concurrent.Thread用法的更多信息,并为高级用户提供了列出文档的信息,是本教程最适合的材料。 还鼓励您检查Concurrent.Thread网站 ,该网站提供了更多信息。
关于作者
aki木大辅(Daisuke Maki) :毕业于国际基督教大学(人文科学学士)的人文科学系自然科学系,然后在电子通信大学的研究生院主修信息技术。 他专门研究Web开发,特别是使用JavaScript的AJAX,开发了Concurrent.Thread。 该项目被日本信息技术振兴机构(IPA)在2006财政年度实施的Explatory Software Project采纳。
他目前正在攻读博士学位。 电子通信大学研究生院课程。 他还拥有工程学硕士学位。
多线程编程和单线程编程