Ajax| Program
As Ajax paradigms get more and more widely used, browser pages can keep the front-end user interface active (and therefore asynchronous in Ajax) while requesting data from the back server. However, problems can arise when both activities access the shared JavaScript and DOM data structures at the same time. JavaScript does not provide a classic solution for this concurrency program problem. This article describes the author's new insights on mutual exclusion mechanisms, which can play a good role in JavaScript.
Why do we need mutual exclusion?
When multiple program logical threads access the same data at the same time, the problem arises. Programs generally assume that the data they interact with does not change during the interaction. The code that accesses these shared data structures is called a critical section, and a mechanism that allows only one program access at a time is called a mutex. In an AJAX application, this occurs when the code that asynchronously handles the response from the XMLHttpRequest simultaneously manipulates data that is being used by the user interface. This shared data may be the DOM of JavaScript and/or Web pages that are used to implement the MVC data model. If either of them makes an uncoordinated change to the shared data, the logic of both will be interrupted.
Perhaps you would say, "Wait, why haven't I encountered this kind of problem?" ”。 Unfortunately, this problem is synchronous-dependent (also called a race condition), so they don't always happen, or perhaps never happen. Their probabilities are based on a number of factors. Based on robustness, rich Internet applications should prevent this by ensuring that these problems do not occur.
Therefore, a mutex is required to ensure that only one critical section can be opened at a time, and the other cannot be opened until it is finished. In most mainstream computer languages and execution frameworks, mutual exclusion mechanisms (often several) are available, but JavaScript applied to the browser side does not provide this mutex. Although there are some classic mutex implementations that do not require specialized language or environment support, even this requires some elements that are missing from JavaScript and browsers, such as Internet Explorer. The classic algorithms described next can play a good role in these browsers and languages.
Bread Shop Algorithm
In several mutual exclusion algorithms in computer science literature, the so-called Lamport Bakery algorithm can be effectively used in multiple competing control threads, the communication between the threads of the algorithm can only be carried out in shared memory (i.e., no special mechanisms such as semaphores, set-and-test, etc.). The basic idea of the algorithm stems from the bakery, because the bakery needs to pick the number and wait for the call. Listing 1 shows the framework of the algorithm, derived from Wikipedia, which allows each thread to enter and leave the critical section without conflict.
Listing 1. Lamport Bakery algorithm pseudo code
Declaration & Initial values of global variables
Enter, Number:array [1..N] of integer = {0};
Logic used by each thread ...
Where "(A, B)" (C, D) "
means "(a C) or ((a = = c) and (b))"
Thread (i) {
while (true) {
Enter [i] = 1;
Number[i] = 1 + max (number[1],..., number[n]);
Enter [i] = 0;
for (j=1; j<=n; ++j) {
while (Enter[j]!= 0) {
Wait until thread J receives its number
}
while ((number[j]!=0) && ((number[j],j) (number[i],i)) {
Wait until threads with smaller numbers
or with the same number, but with higher
Priority, finish their work
}
}
Critical section ...
Number[i] = 0;
Non-critical section ...
}
}
As shown above, the algorithm assumes that each thread knows its own thread number (constant i) and the total number of threads currently active (constant N). In addition, it is assumed that there is a way to wait or hibernate, for example, to temporarily release the CPU to another thread. Unfortunately, JavaScript in Internet Explorer does not have this ability. However, if multiple pieces of code that actually run on the same thread behave as if they were running on separate virtual threads, the bakery algorithm will not break. Similarly, JavaScript has a mechanism for scheduling functions after a specified delay, so you can use the following methods to optimize the bakery algorithm.
Wallace Variant
The main obstacle to implementing the Lamport bakery algorithm in JavaScript is the lack of a thread API. Cannot determine which thread is currently running and the number of threads currently active, nor can the CPU be freed to other threads, and new threads cannot be created to manage other threads. Therefore, it is not possible to verify how specific browser events (for example, click Buttons, available XML answers, and so on) are assigned to threads.
One way to overcome these obstacles is to use command design patterns. You can override the bakery algorithm in the class that is responsible for managing the command by putting all the logic that should go into the critical section and all the data needed to start the logic into the Command object. The mutex only calls the critical extents in the absence of other critical extents (encapsulated as separate command object methods) at execution time, as if they were running in different virtual threads. The settimeout () mechanism of JavaScript is used to release the CPU to other waiting command.
Assuming a simple base class for the command object (see the command in Listing 2), you can define a class (see the mutex in Listing 3) to implement the Wallace variant of the bakery algorithm. Note that although the base class object can be implemented in JavaScript in many ways (for simplicity, this is a simple way), as long as each command object has a unique ID, and the entire critical area is encapsulated in a separate method, Then any object pattern can use this method.
Listing 2. Simple base class for Command object
1 function Command () {
2 if (! Command.nextid) Command.nextid = 0;
3 this.id = ++command.nextid;
4//unsynchronized API
5 This.doit = function () {alert ("doit called");}
6 This.undo = function () {alert ("undo called");}
7 This.redo = function () {This.doit ();}
8//Synchronized API
9 This.sdoit = function () {New Mutex (this, "doit");}
This.sundo = function () {New Mutex (this, "undo");}
One This.sredo = function () {New Mutex (this, "Redo");}
12}
The command class demonstrates three critical section methods (see Line 5-7), but any method can be used whenever a call to the method is encapsulated in a mutex (see line 9-11). It is important to recognize that there is a significant difference between regular method calls, such as asynchronous method calls, and synchronous method calls: Ironically, it is necessary to ensure that synchronization methods are not running in a different step. In other words, when you call the Sdoit () method, you must ensure that the method doit () is not yet running, even if the method Sdoit () has been returned. The doit () method may have ended, or it will not begin execution until some time in the future. That is, the instantiation of the mutex is considered to start a new thread.
Listing 3. Wallace variant implemented as a class mutex
1 function Mutex (Cmdobject, methodname) {
2//define static field and method
3 if (! mutex.wait) mutex.wait = new Map ();
4 Mutex.slice = function (CmdID, Startid) {
5 Mutex.Wait.get (CmdID). Attempt (Mutex.Wait.get (Startid));
6}
7//Define instance method
8 this.attempt = function (start) {
9 for (Var j=start j; J=mutex.wait.next (J.c.id)) {
Ten if (j.enter
11 | | (J.number && j.number This.number | |
(J.number = = This.number
&& j.c.id this.c.id)))
Return settimeout
("Mutex.slice" ("+this.c.id+", "+j.c.id+") ", 10);
16}
//run with exclusive access
this.c[This.methodid] ();
//release Exclusive access
This.number = 0;
Mutex.Wait.remove (this.c.id);
22}
Constructor//Logic
THIS.C = Cmdobject;
This.methodid = methodname;
num//(Enter and number are "false" here)
Mutex.Wait.add (this.c.id, this);
This.enter = true;
This.number = (new Date ()). GetTime ();
This.enter = false;
This.attempt (Mutex.Wait.first ());
32}
The basic logic of a mutex class is to put each new instance of a mutex into the master wait list and then start it in the wait queue. Because every attempt to get to the top of the team needs to wait (except for the last), use settimeout to schedule a new attempt to start each time the current attempt stops. When you arrive at the top of the team (see Line 17), you achieve mutual exclusion access; Therefore, you can call the critical area method. After the critical section has been executed, release the mutex access and remove the mutex instance from the wait list (see line 20-21).
The Mutex constructor (see line 23-31) records its command object and method name parameters and then sends a sparse array (mutex.wait) in a running critical section, which is implemented through the map class shown in Listing 4. The constructor then obtains the next number and queues at the end of the team. The current timestamp is actually used as the next number because there is no problem with the interval or replica in the wait number.
The attempt () method combines two wait loops in the initial pseudocode into a single loop that does not invalidate the critical section until the first team. The loop is a busy-wait loop detection method that terminates the loop by specifying the amount of latency in the settimeout () call. The static helper method (Mutex.slice) is defined on the 4–6 line because settimeout needs to invoke "unformatted functions". Slice finds the specified mutex object in the master wait list and then calls its attempt () method, specifying the length of the wait list it obtains so far by using the start argument. Each slice () call is like a "piece of CPU". This collaborative approach (via settimeout) to release CPUs in a timely fashion is an idea of collaborative programs.
Listing 4. A sparse array implemented as a map data structure
function Map () {
This.map = new Object ();
Map API
This.add = function (k,o) {
This.map[k] = o;
}
This.remove = function (k) {
Delete This.map[k];
}
This.get = function (k) {
Return k==null? NULL:THIS.MAP[K];
}
This.first = function () {
Return This.get (This.nextkey ());
}
This.next = function (k) {
Return This.get (This.nextkey (k));
}
This.nextkey = function (k) {
For (i in This.map) {
if (!k) return i;
if (k==i) k=null; /*tricky*/
}
return null;
}
}
Rich Internet Application integration
Because the number of threads (virtual or non-virtual) that a mutex handles is dynamic, you can determine a basic fact: you cannot get the thread identifier in such a way that browsers can assign individual threads to individual browser events. A similar assumption is made that each complete event handler consists of a complete critical section. Based on these assumptions, each event-handler function can be converted to a command object and managed using a mutex. Of course, if you do not explicitly organize your code into an event handler, you will need to refactor it. In other words, it is not logically encoded in the HTML event attribute (for example: and function FOO () {++var;} )。
Listing 5. Sample Web page with asynchronous event handlers
<script language= "JavaScript" >
function NewState () {
if (xmlreq.readystate==4) processreply ();
}
function RequestData () {
... set up asynchronous XML request ...
Xmlreq.onreadystatechange = newstate;
... launch XML request ...
}
function processreply () {
var Transformeddata = ... process data to HTML ...
outputarea.innerhtml = Transformeddata + "<br>";
}
function Cleararea () {
outputarea.innerhtml = "Cleared <br>";
}
</script>
<body >
<input type= "button" value= "clear"
<div id= "Outputarea"/>
</body>
For example, suppose you have three event handler functions that manipulate the shared data shown in Listing 5. They handle page load events, click button Events, and answer events from XML requests. The page Load event issues an asynchronous request to fetch the data and specify a request-reply event handler that processes the received data and loads it into a shared data structure. Clicking the button event handler also affects the shared data structure. To avoid conflicts between these event handlers, you can turn them into command and invoke them by using the mutex shown in Listing 6 (assuming that the JavaScript include file Mutex.js contains a map and a mutex). Note that although a graceful class inheritance mechanism can be used to implement the command subclass, the code illustrates the simplest method that requires only global variable next_cmd_id.
Listing 6. A Web page converted to a synchronization event handler
<script src= "Mutex.js" > </script>
<script language= "JavaScript" >
function RequestData () {
New Mutex (New Requestdatacmd (), "go"); }
function processreply () {
New Mutex (New Processreplycmd (), "go"); }
function Cleararea () {
New Mutex (New Clearareacmd (), "go"); }
function NewState () {
if (xmlreq.readystate==4) processreply (); }
var next_cmd_id = 0;
function Requestdatacmd () {
This.id = ++next_cmd_id;
This.go = function () {
... set up asynchronous XML request ...
Xmlreq.onreadystatechange = newstate;
... launch XML request ...
}
}
function Processreplycmd () {
This.id = ++next_cmd_id;
This.go = function () {
var Transformeddata = ... process data to HTML ...
outputarea.innerhtml = Transformeddata + "<br>";
}
}
function Clearareacmd () {
This.id = ++next_cmd_id;
This.go = function () {
outputarea.innerhtml = "Cleared <br>"; }
}
</script>
<body >
<input type= "button" value= "clear"
<div id= "Outputarea"/>
</body>
These three event handler functions have been converted to the initial logic that calls them through mutexes (are currently prepackaged in the command Class). Each command class defines a unique identifier and a method that contains the logic of the critical section, which satisfies the requirements of the Command interface.
Concluding remarks
With Ajax and Ria, the drive to build complex dynamic user interfaces is prompting developers to use design patterns (such as model-view-controller) that were previously tightly linked to a rich GUI client. As the definition of the view and controller is modular, and each has its own event and event handlers (in addition to the shared data model), the probability of conflict increases exponentially. By encapsulating event-handling logic into the command class, you can use not only the Wallace variant, but also the conditions for providing rich undo/redo functionality, scripting interfaces, and unit test tools.