In the traditional programming model, I/O operations are like an ordinary local function call: the program is blocked before the function is executed and cannot continue to run. Blocking I/O originated from the earlier time slice model. Under this model, each process is like an independent person. The purpose is to distinguish each person, and each person can usually only do one thing at the same time and must wait. Only when you finish the previous thing can you decide what to do next. But this "one user, one process" model that is widely used in computer networks and the Internet has poor scalability. When managing multiple processes, a lot of memory will be consumed, and context switching will also occupy a lot of resources. These are a huge burden on the operating system, and as the number of processes increases, system performance will decline sharply.
Multi-threading is an alternative. A thread is a lightweight process that shares memory with other threads in the same process. It is more like an extension of the traditional model and is used to execute multiple threads concurrently. When a thread While waiting for an I/O operation, other threads can take over the CPU. When the I/O operation is completed, the previously waiting thread will be awakened. That is, a running thread can be interrupted and then resumed later. In addition, under some systems threads can run in parallel on different cores of a multi-core CPU.
Programmers do not know when the thread will run at a specific time. They must be very careful to handle concurrent access to shared memory. Therefore, they must use some synchronization primitives to synchronize access to a certain data structure, such as using locks or semaphores. Use this to force threads to execute in specific behaviors and plans. Applications that rely heavily on shared state between threads are prone to strange problems that are highly random and difficult to find.
Another way is to use multi-thread collaboration. You are responsible for explicitly releasing the CPU and giving the CPU time to other threads. Because you personally control the execution plan of the thread, you reduce the need for synchronization. requirements, but it also increases the complexity of the program and the chance of errors, and does not avoid the problems of multi-threading.
What is event-driven programming
Event-driven programming (Evnet-driven programming) is a programming style in which events determine the execution flow of the program. Events are processed by event handlers or event callbacks. Event callbacks are the current A function that is called when a specific event occurs, such as the database returning query results or the user clicking a button.
Recall that in the traditional blocking I/O programming model, a database query might look like this:
do_something_with(result);
In an event-driven model, this query would look like this:
do_something_with(result);
}
query('SELECT * FROM posts WHERE id = 1', query_finished);
First you define a function called query_finished, which contains what to do after the query is completed. Then pass this function as a parameter to the query function. When the query is executed, query_finished will be called instead of just returning the query results.
When the event you are interested in occurs, the function you define will be called instead of simply returning the result value. This programming model is called event-driven programming or asynchronous programming. This is one of the most obvious features of Node. This programming model means that the current process will not be blocked when performing I/O operations. Therefore, multiple I/O operations can be executed in parallel. When the operation is completed, the corresponding callback function will will be called.
The bottom layer of event-driven programming relies on the event loop. The event loop is basically a structure that continuously calls two functions: event detection and event processor triggering. In each loop, the event loop mechanism needs to detect which events have occurred. When the event occurs, it finds the corresponding callback function and calls it.
The event loop is just a thread running within the process. When an event occurs, the event handler can run alone and will not be interrupted, that is:
1. At most one event callback function can be run at a specific moment
2. Any event handler will not be interrupted while running
With this, developers can no longer worry about thread synchronization and concurrent modification of shared memory.
A well-known secret:
People in the systems programming community have known for a long time that event-driven programming is the best way to create high-concurrency services because it doesn’t have to save a lot of context, so it saves a lot of memory, it doesn’t have so many context switches, and it saves a lot of execution. time.
Slowly, this concept has penetrated into other platforms and communities, and some famous event loop implementations have emerged, such as Ruby's Event machine, Perl's AnyEvnet, and Python's Twisted. In addition to these, there are many other implementations and language.
To develop with these frameworks, you need to learn specific knowledge related to the framework and framework-specific class libraries. For example, when using Event Machine, in order to enjoy the benefits of non-blocking, you have to avoid using synchronization class libraries and can only use Asynchronous class library for Event Machine. If you use any blocking libraries (such as most of Ruby's standard libraries), your server loses optimal scalability because the event loop will still be constantly blocked, occasionally preventing the processing of I/O events. .
Node was originally designed as a non-blocking I/O server platform, so in general, you should expect all code running on it to be non-blocking. Because JavaScript is very small and it does not enforce any I/O model (because it does not have a standard I/O library), Node is built in a very pure environment without any legacy issues.
How Node and JavaScript simplify asynchronous applications
Ryan Dahl, the author of Node, initially used C to develop this project, but found that maintaining the context of function calls was too complicated, resulting in high code complexity. Then he switched to Lua, but Lua already has several blocking I/O libraries. Mixing blocking and non-blocking may confuse developers and prevent many people from building scalable applications, so Lua was also Dahl abandoned. Finally, he turned to JavaScript. Closures and first-level object functions in JavaScript made JavaScript very suitable for event-driven programming. The magic of JavaScript is one of the main reasons why Node is so popular.
What is a closure
A closure can be understood as a special function, but it can inherit and access variables in the scope in which it is defined. When you pass a callback function as a parameter to another function, it will be called later. The magic is that when the callback function is called later, it actually remembers the context in which it is defined and the parent context. variables in it, and they can be accessed normally. This powerful feature is at the core of Node's success.
The following example shows how JavaScript closures work in a web browser. If you want to listen to the stand-alone event of a button, you can do this:
document.getElementById('myButton').onclick = function() {
clickCount = 1;
alert("clicked " clickCount " times.");
};
This is how it works when using jQuery:
$('button#mybutton').click(function() {
clickedCount ;
alert('Clicked ' clickCount ' times.');
});
In JavaScript, functions are first-class objects, which means you can pass functions as parameters to other functions. In the two examples above, the former assigns a function to another function, and the latter passes the function as a parameter to another function. The handler function (callback function) of the click event can access every variable under the code block where the function is defined. , in this case, it has access to the clickCount variable defined within its parent closure.
The clickCount variable is in the global scope (the outermost scope in JavaScript). It saves the number of times the user clicks the button. It is usually a bad habit to store variables in the global scope because it can easily conflict with other code. , you should place variables in the local scope where they are used. Most of the time, just wrapping the code in a function is equivalent to creating an additional closure, so that you can easily avoid polluting the global environment, like this:
var clickCount = 0;
$('button#mybutton').click(function() {
clickCount ;
alert('Clicked ' clickCount ' times.');
}());
Note: The seventh line of the above code defines a function and calls it immediately. This is a common design pattern in JavaScript: creating a new scope by creating a function.
How closures help asynchronous programming
In the event-driven programming model, first write the code that will be run after the event occurs, then put the code into a function, and finally pass the function as a parameter to the caller, which will later be called by the caller function.In JavaScript, a function is not an isolated definition. It will also remember the context of the scope in which it is declared. This mechanism allows JavaScript functions to access the context where the function is defined and the parent context. all variables.
When you pass a callback function as a parameter to the caller, this function will be called at a later time. Even if the scope in which the callback function is defined has ended, when the callback function is called, it can still access all variables in the ended scope and its parent scope. Like the last example, the callback function is called inside jQuery's click(), but it can still access the clickCount variable.
The magic of closures was shown earlier. Passing state variables to a function allows you to perform event-driven programming without maintaining state. JavaScript's closure mechanism will help you maintain them.
Summary
Event-driven programming is a programming model that determines the program execution flow through event triggering. Programmers register callback functions (often called event handlers) for the events they are interested in, and the system then calls the registered event handler when the event occurs. This programming model has many advantages that the traditional blocking programming model does not have. In the past, to achieve similar features, you had to use multi-process/multi-threading.JavaScript is a powerful language because its function and closure properties of first-type objects make it well-suited for event-driven programming.