沙漠中的鱼

欲上天堂,先下地狱
posts - 0, comments - 56, trackbacks - 0, articles - 119
  BlogJava :: 首页 ::  :: 联系 :: 聚合  :: 管理

JavaScript多线程技术

Posted on 2008-07-17 17:23 沙漠中的鱼 阅读(1867) 评论(2)  编辑  收藏 所属分类: javascript

While increasingly more websites are fully or partially based on AJAX, it is still difficult to develop complicated AJAX applications. What is the main issue which causes this difficulty in developing AJAX applications? Is it asynchronous communication with the server, or is it GUI programming? Both are routinely performed by desktop window applications -- so why is development of AJAX applications which do the same things particularly difficult?

Let’s consider this issue using a simple example. Suppose that you want to build a tree-structured bulletin board system which loads the data for each article by communicating with the server according to user requests rather than loading all articles at one time from the server. Each article has 4 pieces of information associated with it: a unique ID within the bulletin board system, the name of the person who posted the article, the content of the article, and an array of the IDs of its child articles. To begin with, let’s assume that there is a JavaScript function named getArticle() which is responsible for loading a single article. This function receives the integer ID of the article to be loaded as an argument, and it retrieves the data of the article with that ID from the server. It then returns an object which has the 4 pieces of information contained in that article: id, name, content, and children. An example of this function in use can be written like this:

function ( id ) {
var a = getArticle(id);
document.writeln(a.name + "<br>" + a.content);
}

As you may notice, calling this function many times with the same article ID, however, requires communications with the server again and again for no good reason. To counter this problem, consider the function getArticleWithCache(), which is a getArticle() with a cache capability. In this example, the data loaded by getArticle() will be simply retained as a global variable:

var cache = {};
function getArticleWithCache ( id ) {
if ( !cache[id] ) {
cache[id] = getArticle(id);
}
return cache[id];
}

Now the articles that have been read are cached. Now, let’s consider the function backgroundLoad(), which loads the data of all articles based on this mechanism. This function aims to preload all of the child articles in the background while the user is reading a given article. Because the article data is tree structured, a recursive algorithm can easily be written which traverses the tree and allows all articles to be loaded:

function backgroundLoad ( ids ) {
for ( var i=0; i < ids.length; i++ ) {
var a = getArticleWithCache(ids[i]);
backgroundLoad(a.children);
}
}

The backgroundLoad() function receives an array of IDs as an argument and applies our previously-defined getArticleWithCache() to each ID. This allows the data of the article corresponding to each ID to be cached. Then, by recursively calling backgroundLoad() on the IDs of the child articles of the loaded article, the entire article tree is cached.

So far, everything looks good. If you have worked on AJAX application development, however, you should know that this naïve implementation won’t work successfully. The example has been based on the tacit understanding that getArticle() uses synchronous communication. As a general rule, however, JavaScript requires the use of asynchronous communication in communicating with the server because it has only a single thread. In terms of simplicity, processing everything (including GUI events and rendering), on one thread is a good programming model, because it eliminates the need to think about the complicated problems associated with thread synchronization. On the other hand, it presents a significant problem in developing applications –which appear responsive to the user because the single-thread environment cannot respond to users’ mouse clicking and/or key operation when the thread is working on something else (such as the getArticle() call).

What happens if synchronous communication is carried out within this single-threaded environment? Synchronous communication stops the browser’s execution until the communication result is obtained. The thread cannot respond to users while waiting for the communication result because the call from the server has not been completed, and the thread will remain blocked until the call returns. For this reason, it can not respond to users while it is waiting for the server’s response and the browser therefore looks frozen. This also holds true for the execution of getArticleWithCache() and backgroundLoad(), which are based on getArticle(). Because may take a considerable amount of time to download all of the articles, the browser freezing during that time is a serious problem for backgroundLoad() - since the browser is frozen, it is not possible in the first place to achieve the goal of preloading the data in the background while users are reading articles, since the article will be unreadable.

Since the use of synchronous communication creates a significant problem in usability as described above, JavaScript uses asynchronous communication as a general rule. Therefore, let’s rewrite the program above based on asynchronous communication. JavaScript requires asynchronous communications to be written in an event-driven programming style. In most instances, you specify a callback function which is called once the communication response has been received. For example, getArticleWithCache() defined above can be rewritten as:

var cache = {};
function getArticleWithCache ( id, callback ) {
if ( !cache[id] ) {
callback(cache[id]);
} else {
getArticle(id, function( a ){
cache[id] = a;
callback(a);
});
}
}

This program also internally calls the getArticle() function. It should be noted, however, that the version of getArticle() which is designed for asynchronous communication expects to receive a function as the second argument. When this version of getArticle() is called, it sends a request to the server, as before, however the function returns immediately without waiting for a response from the server. This means that when the execution is returned to the caller, the server response has not yet been retrieved. This allows the thread to work on other tasks until the server response is obtained and the callback function is called. As soon as this response is received from the server, the callback function specified as the second argument of getArticle() is invoked with the server’s response as an argument. Likewise, getArticleWithCache() has been changed so that it will expect a callback function as the second argument. This callback function will then be called within the callback function that is passed to getArticle() so that it will be executed after the server-communication is finished.

You may think that the above rewriting alone is rather complicated, but the backgroundLoad() function involves even more complicated rewriting. It can be also rewritten to handle a callback function:

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();
}

This rewritten backgroundLoad() function does not look much like our original function, however there is no difference in what they do. This means that both functions receive an array of IDs, call getArticleWithCache() on each element of the array, and recursively apply backgroundLoad() to the resultant child articles. However, it is not easy to recognize even the loop structure for the array, which was represented by a for-statement in the original program. Why are these two sets of functions that do the same thing so totally different from each other?

The difference results from the fact that any function must return immediately after any function that requires server-communication, such as getArticleWithCache(). The callback function that should receive the server response cannot be called unless the original function is no longer executing. For JavaScript, it is not possible to suspend the program in the middle of loops, such as for-statements, and resume it later at the point where execution was suspended; the loop is therefore represented by recursively passing the callback function instead of using a loop syntax. For those who are familiar with Continuation-Passing Style (CPS), this is a manual implementation of CPS. Because no loop syntax can be used, even the simple program described earlier that traverses a tree requires complicated statements. The problem associated with event-driven programs is known as the control flow problem: loop and other control flow statements are likely to be difficult to understand.

There is another problem: if you convert a function which does not use asynchronous communication into a function that uses asynchronous communication, the rewritten function will need to have a new parameter which is a callback functions. This poses a significant problem to existing APIs since our internal changes will not remain internal, but will result in broken APIs and changes by others using our API.

What is the root cause of all of these problems? That’s right. The fact that JavaScript has only one thread causes the problems. Carrying out asynchronous communication on only one thread requires an event-driven program and complicated statements. If another thread could respond to users while the program is waiting for the server response, acrobatics like this would not be required.

Invitation to Multithreaded Programming

Let me talk about Concurrent.Thread, a library that allows JavaScript to use multiple threads, since this greatly eases the difficulty associated with asynchronous communication in the AJAX development mentioned above. This is a free-software library implemented in JavaScript, available under the Mozilla Public License / GNU General Public License. You can download the source code from the website.

Let’s download and use the source code right away. Suppose that you have saved the downloaded source code as a file named Concurrent.Thread.js. Before doing anything else, let’s run the program below, which has a very naïve implementation:

<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>

Executing this program should display numbers starting with 0 in order. Numbers appear one after another, which you can view by scrolling the page. Now, let’s look at the source code in more detail. It uses a simple infinite loop as indicated by while ( 1 ). In ordinary cases, a JavaScript program like this continues to use the one and only thread, causing the browser to look frozen. Naturally, it does not allow you to scroll the screen. Then, why does the above program allow you to scroll? The key is Concurrent.Thread.create() located above while ( 1 ). This is a method provided by the library; it is for creating a new thread. On a new thread, the function passed as the argument is executed. Let me slightly rewrite the program as follows:

<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>

In this program we have a new function f(), which shows numbers repeatedly. This is defined at the top, and the create() method is called twice with f() as arguments. The second argument passed to the create() method is passed to f() without modification. Executing this program shows some small numbers starting with 0, which are followed by some large numbers starting with 100,000 and small numbers again that follow up the first series of small numbers. Like this, you can observe that the program shows alternating lines of small numbers and large numbers. This indicates that two threads are running concurrently.

Let me show you another use of Concurrent.Thread. In the above example, the create() method was called to create a thread. It is also possible to create a thread without calling any library APIs at all. For example, the former example can be expressed as:

<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>

Inside the script tag, an infinite loop is written simply in JavaScript. You should take note of the type attribute of the tag: an unfamiliar value (text/x-script.multithreaded-js) is assigned to it. If this attribute is assigned to the script tag, then Concurrent.Thread executes the content of the tag on a new thread. You should remember that, in this case as well, the library body of Concurrent.Thread must be included.

With Concurrent.Thread, it is possible to switch execution context from one thread to another as needed even if you write a long and continuous program. Let me briefly talk about how this behavior is achieved. In short, code conversion is used. Very roughly speaking, the function passed to the create() method is first converted to a character string, which is then rewritten so that it can be executed on a piecemeal basis. Then, the rewritten function is executed little by little on the scheduler. The scheduler is responsible for coordinating multiple threads. In other words, it makes adjustments so that each of the rewritten functions will be evenly executed. Concurrent.Thread actually does not create new threads but simply simulates a multi-threaded environment on the original single thread.

Although the converted functions appear to be running on different threads, there is actually only one thread running everything. Carrying out synchronous communication within the converted functions will still cause the browser to freeze. You may think that our original problem has not been solved at all, however you don’t have to worry. Concurrent.Thread provides a purpose-built communications library which is implemented using the asynchronous JavaScript communication style and which is designed to allow the other threads to work even when a thread is waiting for a response from the server. This communications library is found under the Concurrent.Thread.Http namespace. For example, it is used as follows:

<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>

The get() method retrieves the content of the specified URL using HTTP GET, as its name suggests. It takes a target URL as the first argument and an array representing HTTP header fields as the optional second argument. The get() method communicates with the server and returns an XMLHttpRequest object as the return value when it has received the server response. When the get() method returns, the response had been received. It is not necessary to use a callback function to receive the result. Naturally, there is no worry that the browser freezes while the program is waiting a response from the server. In addition, the post() method can be used to send data to the server:

<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>

The 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.

If you implement getArticle() in the first example using this communications library, then you can quickly write getArticleWithCache(), backgroundLoad(), and other functions that use getArticle() using the naïve method shown at the beginning of this article. Even when that version of backgroundLoad() is reading article data, another thread can respond to users as a matter of course, and the browser therefore does not freeze. Now, do you understand how useful it is to use multiple threads in JavaScript?


 

转载:http://www.infoq.com/articles/js_multithread


评论

# re: JavaScript多线程技术  回复  更多评论   

2011-01-25 16:55 by exist
可否汉化?

# re: JavaScript多线程技术  回复  更多评论   

2011-03-25 17:29 by ppboy
http://www.infoq.com/cn/articles/js_multithread

只有注册用户登录后才能发表评论。


网站导航: