javascript asynchronous operation, serial execution, parallel execution

Single- threaded model
The single-threaded model means that JavaScript runs on only one thread. That is to say, JavaScript can only execute one task at a time, and all other tasks must be queued in the back.

Note that JavaScript only runs on one thread, it does not mean that the JavaScript engine has only one thread. In fact, the JavaScript engine has multiple threads, and a single script can only run on one thread (called the main thread), and other threads cooperate in the background.

The reason why JavaScript is single-threaded rather than multi-threaded is related to history. JavaScript has been single-threaded since its inception. The reason is that it does not want to make the browser too complicated, because multiple threads need to share resources and may modify each other's running results. For a web scripting language, this is too complicated. If JavaScript has two threads at the same time, one thread adds content to the DOM node of the web page, and the other thread deletes the node, which thread should the browser take? Is there still a locking mechanism? So, to avoid complexity, JavaScript was single-threaded from the start, which has become a core feature of the language and will not change in the future.

The advantage of this mode is that it is relatively simple to implement and the execution environment is relatively simple; the disadvantage is that as long as one task takes a long time, the subsequent tasks must be queued up, which will delay the execution of the entire program. Common browsers are unresponsive (feigned death), often because a certain piece of JavaScript code runs for a long time (such as an infinite loop), causing the entire page to be stuck in this place, and other tasks cannot be performed. The JavaScript language itself is not slow. What is slow is reading and writing external data, such as waiting for an Ajax request to return the result. At this time, if the other server does not respond slowly, or the network is not smooth, it will cause the script to stagnate for a long time.

If the queuing is due to the large amount of computation and the CPU is too busy, it’s okay, but many times the CPU is idle, because the IO operation (input and output) is very slow (such as Ajax operation reading data from the network), you have to wait After the result comes out, go to the next execution. The designers of the JavaScript language realized that at this time, the CPU can completely ignore the IO operation, suspend the waiting tasks, and run the tasks in the back first. Wait until the IO operation returns the result, then go back and continue to execute the suspended task. This mechanism is the "Event Loop" mechanism used inside JavaScript.

The single-threaded model, while limiting JavaScript, also gives it advantages over other languages. If used well, JavaScript programs will not be blocked, which is why Node can handle large traffic with very few resources.

In order to take advantage of the computing power of multi-core CPUs, HTML5 proposes the Web Worker standard, which allows JavaScript scripts to create multiple threads, but the child threads are completely controlled by the main thread and must not operate the DOM. So, this new standard doesn't change the single-threaded nature of JavaScript.

Synchronous tasks and asynchronous tasks
All tasks in the program can be divided into two categories: synchronous tasks and asynchronous tasks.

Synchronous tasks are those tasks that are not suspended by the engine and are queued for execution on the main thread. The next task can only be executed after the previous task has been executed.

Asynchronous tasks are those tasks that are put aside by the engine and do not enter the main thread, but enter the task queue. Only when the engine thinks that an asynchronous task can be executed (for example, an Ajax operation gets the result from the server), the task (in the form of a callback function) will enter the main thread for execution. The code after the asynchronous task will run immediately without waiting for the asynchronous task to finish, that is, the asynchronous task does not have a "blocking" effect.

For example, Ajax operations can be handled as synchronous tasks or as asynchronous tasks, it is up to the developer. If it is a synchronous task, the main thread waits for the Ajax operation to return the result, and then executes it; if it is an asynchronous task, after the main thread sends an Ajax request, it will directly execute it, and wait until the Ajax operation has the result, and the main thread will execute it again. The corresponding callback function.

Task queue and event loop
When JavaScript is running, in addition to a running main thread, the engine also provides a task queue (task queue), which contains various asynchronous tasks that need to be processed by the current program. (Actually, there are multiple task queues depending on the type of asynchronous tasks. For ease of understanding, it is assumed here that only one queue exists.)

First, the main thread will perform all synchronization tasks. When all the synchronous tasks are executed, the asynchronous tasks in the task queue will be viewed. If the conditions are met, the asynchronous task re-enters the main thread to start executing, and then it becomes a synchronous task. After the execution is completed, the next asynchronous task enters the main thread to start execution. Once the task queue is emptied, the program ends execution.

Asynchronous tasks are usually written as callback functions. Once the asynchronous task re-enters the main thread, the corresponding callback function is executed. If an asynchronous task does not have a callback function, it will not enter the task queue, that is, it will not re-enter the main thread, because the callback function is not used to specify the next operation.

How does the JavaScript engine know whether the asynchronous task has a result or not, and can it enter the main thread? The answer is that the engine is constantly checking, over and over again, as long as the synchronous task is executed, the engine will check whether the suspended asynchronous tasks can enter the main thread. This mechanism of loop checking is called the event loop. Wikipedia defines it as: "An event loop is a programming construct that waits for and dispatches events or messages in a program".

Modes of Asynchronous Operation The
following summarizes several modes of asynchronous operation.

Callback function
The callback function is the most basic method of asynchronous operation.

The following are two functions f1 and f2. The intention of programming is that f2 must wait until f1 is executed before it can be executed.

function f1() {
    
    
  // ...
}

function f2() {
    
    
  // ...
}

f1();
f2();

The problem with the above code is that if f1 is an asynchronous operation, f2 will be executed immediately and will not wait until f1 ends.

At this time, you can consider rewriting f1 and writing f2 as the callback function of f1.

function f1(callback) {
    
    
  // ...
  callback();
}

function f2() {
    
    
  // ...
}

f1(f2);

The advantage of the callback function is that it is simple, easy to understand and implement, but the disadvantage is that it is not conducive to the reading and maintenance of the code. The high coupling between various parts makes the program structure chaotic and the process difficult to track (especially if multiple callback functions are nested). situation), and only one callback function can be specified per task.

Event monitoring
Another way of thinking is to use an event-driven model. The execution of asynchronous tasks does not depend on the order of the code, but on whether an event occurs.

Take f1 and f2 as an example. First, bind an event to f1 (the jQuery way of writing is used here).

1 f1.on('done', f2);

The above line of code means that when a done event occurs in f1, f2 is executed. Then, rewrite f1:

1 function f1() {
    
    
2   setTimeout(function () {
    
    
3     // ...
4     f1.trigger('done');
5   }, 1000);
6 }

In the above code, f1.trigger('done') means that after the execution is completed, the done event is triggered immediately, thereby starting to execute f2.

The advantage of this method is that it is easier to understand, multiple events can be bound, multiple callback functions can be specified for each event, and it can be "decoupled", which is conducive to the realization of modularization. The disadvantage is that the entire program has to become event-driven, and the running process will become very unclear. When reading the code, it's hard to see the main flow.

Publish/subscribe
events can be completely understood as "signals". If there is a "signal center" and a task is completed, a signal will be "published" to the signal center, and other tasks can be "subscribed" to the signal center. ) this signal, so as to know when it can start executing. This is called the "publish-subscribe pattern", also known as the "observer pattern".

There are several implementations of this pattern, the following is Tiny Pub/Sub by Ben Alman, which is a plugin for jQuery.

First, f2 subscribes the done signal to the signal center jQuery.

1 jQuery.subscribe('done', f2);

Then, f1 is rewritten as follows.

1 function f1() {
    
    
2   setTimeout(function () {
    
    
3     // ...
4     jQuery.publish('done');
5   }, 1000);
6 }

In the above code, jQuery.publish('done') means that after the execution of f1 is completed, the done signal is released to the signal center jQuery, thereby triggering the execution of f2.

After f2 finishes executing, you can unsubscribe.

1 jQuery.unsubscribe('done', f2);

This method is similar in nature to "event listener", but is significantly better than the latter. Because you can monitor the operation of the program by looking at the "message center" to see how many signals exist and how many subscribers each signal has.

Flow Control
of Asynchronous Operations If there are multiple asynchronous operations, there is a problem of flow control: how to determine the order in which the asynchronous operations are executed, and how to ensure that this order is respected.

1 function async(arg, callback) {
    
    
2   console.log('参数为 ' + arg +' , 1秒后返回结果');
3   setTimeout(function () {
    
     callback(arg * 2); }, 1000);
4 }

The async function of the above code is an asynchronous task, which is very time-consuming. Each execution takes 1 second to complete, and then the callback function is called.

If there are six such asynchronous tasks, all of them need to be completed before the final final function can be executed. How should the operation process be arranged?

function final(value) {
    
    
  console.log('完成: ', value);
}

async(1, function(value){
    
    
  async(value, function(value){
    
    
    async(value, function(value){
    
    
      async(value, function(value){
    
    
        async(value, function(value){
    
    
          async(value, final);
        });
      });
    });
  });
});

In the above code, the nesting of six callback functions is not only troublesome to write, error-prone, but also difficult to maintain.

Serial execution
We can write a flow control function to control asynchronous tasks, and after one task is completed, execute another. This is called serial execution.

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

function async(arg, callback) {
    
    
  console.log('参数为 ' + arg +' , 1秒后返回结果');
  setTimeout(function () {
    
     callback(arg * 2); }, 1000);
}

function final(value) {
    
    
  console.log('完成: ', value);
}

function series(item) {
    
    
  if(item) {
    
    
    async( item, function(result) {
    
    
      results.push(result);
      return series(items.shift());
    });
  } else {
    
    
    return final(results[results.length - 1]);
  }
}

series(items.shift());

In the above code, the function series is a serial function, it will execute asynchronous tasks in sequence, and the final function will be executed after all tasks are completed. The items array holds the parameters of each asynchronous task, and the results array holds the running result of each asynchronous task.

Note that the above writing takes six seconds to complete the entire script.

Parallel execution
The process control function can also be executed in parallel, that is, all asynchronous tasks are executed at the same time, and the final function is executed after all the tasks are completed.

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

function async(arg, callback) {
    
    
  console.log('参数为 ' + arg +' , 1秒后返回结果');
  setTimeout(function () {
    
     callback(arg * 2); }, 1000);
}

function final(value) {
    
    
  console.log('完成: ', value);
}

items.forEach(function(item) {
    
    
  async(item, function(result){
    
    
    results.push(result);
    if(results.length === items.length) {
    
    
      final(results[results.length - 1]);
    }
  })
});

In the above code, the forEach method will initiate six asynchronous tasks at the same time, and the final function will not be executed until they are all completed.

In contrast, the above writing takes only one second to complete the entire script. That is to say, parallel execution is more efficient and saves time compared to serial execution that can only execute one task at a time. But the problem is that if there are many parallel tasks, it is easy to exhaust system resources and slow down the running speed. So there is a third way of flow control.

The combination of
parallel and serial The so-called combination of parallel and serial is to set a threshold, at most n asynchronous tasks can be executed in parallel each time, so as to avoid excessive occupation of system resources.

var items = [1, 2, 3, 4, 5, 6];
var results = [];
var running = 0;
var limit = 2;

function async(arg, callback) {
    
    
  console.log('参数为 ' + arg +' , 1秒后返回结果');
  setTimeout(function () {
    
     callback(arg * 2); }, 1000);
}

function final(value) {
    
    
  console.log('完成: ', value);
}

function launcher() {
    
    
  while(running < limit && items.length > 0) {
    
    
    var item = items.shift();
    async(item, function(result) {
    
    
      results.push(result);
      running--;
      if(items.length > 0) {
    
    
        launcher();
      } else if(running == 0) {
    
    
        final(results);
      }
    });
    running++;
  }
}

launcher();

In the above code, at most two asynchronous tasks can be run at the same time. The variable running records the number of tasks currently running. As long as it is lower than the threshold value, a new task will be started. If it is equal to 0, it means that all tasks have been executed, and then the final function will be executed.

This code takes three seconds to complete the entire script, between serial execution and parallel execution. By adjusting the limit variable, the best balance of efficiency and resources is achieved.

-------------------------------------------------- Refer to Ruan Yifeng's tutorial

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324343153&siteId=291194637