10 pictures to help you thoroughly understand callback functions

I wonder if you also have this doubt, why do we need the concept of callback function? Isn’t it enough to just call the function directly? What exactly does the callback function do? How should programmers understand callback functions?

This article will answer these questions for you. After reading this article, you will have a new powerful weapon in your arsenal .

Everything starts with this need

Suppose your company wants to develop the next generation national app "Tomorrow You Tiao", an app that mainly solves the national breakfast problem. In order to speed up the development progress, this app is jointly developed by Team A and Team B. There is a core module developed by Team A and then called by Team B. This core module is encapsulated into a function, which is called make_youtiao(). If the make_youtiao() function executes quickly and returns immediately, then the students in Group B only need to:

  1. Call make_youtiao()
  2. Wait for the function to complete execution
  3. After the function is executed, continue the subsequent process

From the perspective of program execution, the process looks like this:

  1. Save the context of the currently executed function
  2. Start executing the make_youtiao() function
  3. After make_youtiao() is executed, control is transferred back to the calling function

If all the functions in the world were as simple as make_youtiao(), then programmers would most likely be unemployed. Fortunately, the world of programming is complex, so that programmers have the value of existence.

reality is not easy

In reality, the data that the make_youtiao() function needs to process is very large. Assuming there are 10,000 data, then make_youtiao(10000) will not return immediately , but may take 10 minutes to complete and return. What should you do at this time? Think about this question. Some students may ask, can't we just call it directly like before? It's so simple. Yes, there is nothing wrong with that, but as Einstein said "Everything should be as simple as possible, but not too simple." Think about what would happen if you call it directly? Obviously, if called directly, the calling thread will be blocked and suspended, and can continue to run after waiting for 10 minutes. During these 10 minutes, the thread will not be allocated CPU by the operating system, which means that the thread will not get any advancement. This is not an efficient approach. No programmer wants to stare at a screen for 10 minutes before getting results. So is there a more efficient way? We already know that this pattern of waiting until another task is completed is called synchronization. If you were the boss, would you do nothing and watch your employees write code all the time? Therefore, a better approach is for the programmer to do whatever the boss should do while the programmer is coding. After the programmer finishes writing, he will naturally notify the boss, so that neither the boss nor the programmer need to wait for each other. This mode is called asynchronous. Back to our topic, a better way here is to call the make_youtiao() function and no longer wait for the function to complete , but directly return to continue the subsequent process, so that the program of team A can interact with the make_youtiao() function. At the same time, like this:

In this case, callback must appear.

Why do we need callbacks

Some students may not understand why callback is needed in this case. Don’t worry, we will explain it slowly. Suppose the first version of our "Tomorrow Youtiao" App code is written like this:

make_youtiao(10000);
sell();

You can see that this is the simplest way to write it. The meaning is very simple. Make the fried dough sticks and sell them.

We already know that the function make_youtiao(10000) takes 10 minutes to return. You don’t want to stare at the screen for 10 minutes waiting for the result. So a better way is to let the function make_youtiao() know what to do after making the dough sticks. That is, a better way to call make_youtiao is as follows: "Make 10,000 fried dough sticks, fry them and sell them ." Therefore, calling make_youtiao will look like this:

make_youtiao(10000, sell);

You see, now the make_youtiao function has one more parameter. In addition to specifying the number of fried dough sticks to make, you can also specify what to do after making them . The second function called by the make_youtiao function is called callback. Now you should be able to see that although the sell function is defined by you, this function is called and executed by other modules, like this:

How is the function make_youtiao implemented? It is very simple:

void make_youtiao(int num, func call_back) {
    // 制作油条
    call_back(); //执行回调 
}

In this way, you don't have to stare at the screen, because you delegate the tasks that should be done after the make_youtiao function is executed to the make_youtiao function. The function will know what to do after making the dough sticks, thus freeing up your program. Some students may still have questions, why didn't the team that wrote make_youtiao directly define the sell function and then call it? Don’t forget that the Tomorrow Youtiao App was developed by Team A and Team B at the same time. How did Team A know how Team B would use this module when writing make_youtiao? Assuming that Team A really defines the sell function by itself, it would write it like this:

void make_youtiao(int num) {
    real_make_youtiao(num);
    sell(); //执行回调 
}

At the same time, the module designed by Team A is very easy to use. At this time, Team C also wants to use this module. However, Team C’s demand is to make the dough sticks and put them in the warehouse instead of selling them directly. To meet this demand, what should Team A do? What about writing?

void make_youtiao(int num) {
    real_make_youtiao(num);
    
    if (Team_B) {
       sell(); // 执行回调
    } else if (Team_D) {
       store(); // 放到仓库
    }
}

The story is not over yet. Suppose Team D wants to use it again at this time. Do we need to add if else? In this case, the students in Group A only need to maintain the make_youtiao function to achieve a full workload. Obviously this is a very bad design. So you will see that what to do next after making the fried dough sticks is not something that the A team that implements make_youtiao should be concerned about. Obviously, it can only be known by calling the make_youtiao function. Therefore, Team A of make_youtiao can completely use the callback function to hand over what to do next to the caller. Students in Team A only need to program based on the abstract concept of the callback function, so that the caller does not matter after making the fried dough sticks. You can do whatever you want, whether you sell it, put it in inventory, eat it yourself, etc. Team A's make_youtiao function does not need to make any changes at all , because Team A is programmed based on the abstract concept of callback function. That's what the callback function does, and of course this is where the power of the idea of ​​programming for abstractions rather than concrete implementations lies. Polymorphism in object-oriented essentially allows you to program against abstraction rather than implementation.

Asynchronous callback

The story doesn't end here. In the above example, although we use the concept of callback, that is, the caller implements the callback function and then passes the function as a parameter to other module calls. However, there is still a problem here, that is, the calling method of the make_youtiao function is still synchronous, which means that the caller implements it like this:

make_youtiao(10000, sell);
// make_youtiao函数返回前什么都做不了

We can see that the caller must wait for the make_youtiao function to return before continuing the subsequent process. Let's take a look at the implementation of the make_youtiao function:

void make_youtiao(int num, func call_back) {
    real_make_youtiao(num);
    call_back(); //执行回调 
}

You see, since we have to make 10,000 fried dough sticks, it takes 10 minutes for the make_youtiao function to be executed. That is to say, even if we use a callback, the caller does not need to care about the subsequent process after making the fried dough sticks, but the caller will still be blocked. 10 minutes, that's the problem with synchronous calls. If you really understand the previous section, you should be able to think of a better way. Yes, that's an asynchronous call . Anyway, the follow-up process after making youtiao is not what the caller should care about. In other words, the caller does not care about the return value of the make_youtiao function. So a better way is to put the task of making youtiao into On another thread (process), or even on another machine . If implemented using threads, then make_youtiao is implemented like this:

void make_youtiao(int num, func call_back) {
    // 在新的线程中执行处理逻辑
    create_thread(real_make_youtiao,
                  num,
                  call_back);
}

You see, when we call make_youtiao, it will return immediately . Even if the fried dough sticks have not actually started to be made, the caller does not need to wait for the process of making fried dough sticks, and can immediately execute the post-process:

make_youtiao(10000, sell);
// 立刻返回
// 执行后续流程

At this time, the caller's subsequent process can be carried out at the same time as making fried dough sticks . This is an asynchronous call of the function . Of course, this is also the efficiency of asynchronous.

 Information Direct: Linux kernel source code technology learning route + video tutorial kernel source code

Learning Express: Linux Kernel Source Code Memory Tuning File System Process Management Device Driver/Network Protocol Stack

New programming thinking model

Let's take a closer look at this process. The thinking mode most familiar to programmers is this:

  • Call a function and get the result
  • Process the obtained results
res = request();
handle(res);

This is a synchronous call of the function. Only after the request() function returns and gets the result, can the handle function be called for processing. We must wait before the request function returns. This is a synchronous call, and its control flow is as follows:

But if we want to be more efficient, then we need to call it asynchronously. We do not call the handle function directly, but pass it to the request as a parameter:

request(handle);

We don't care at all when the request actually gets the results. This is what the request should care about. We only need to tell the request what to do after getting the results, so the request function can return immediately and really get the results. The processing may be completed on another thread, process, or even another machine. This is an asynchronous call, and its control flow is like this:

From a programming perspective, there is a big difference between asynchronous calls and synchronization. If we regard the processing process as a task, then the entire task under synchronization is implemented by us, but in the asynchronous case, the processing flow of the task is divided into Two parts:

  1. The first part is what we deal with, that is, the part before calling request
  2. The second part is not processed by us, but processed on other threads, processes, or even another machine.

We can see that since the task is divided into two parts, the call of the second part is not within our control, and only the caller knows what to do, so in this case the callback function is a necessary mechanism. In other words, the essence of the callback function is "Only we know what to do, but we don't know when to do it. Only other modules know, so we must encapsulate what we know into a callback function and tell other modules." Now you should be able to see the difference between the programming mindset of asynchronous callbacks and synchronization. Next, let’s give a more academic definition of callback.

formal definition

In computer science, a callback function is a piece of executable code that is passed as parameters to other code.

This is the definition of the callback function.

The callback function is just a function, no different from other functions. Note that the callback function is a software design concept and has nothing to do with a certain programming language. Almost all programming languages ​​can implement callback functions. For general functions, the functions we write ourselves will be called within our own program, which means that the writer of the function is ourselves and the caller is also ourselves. But the callback function is not like this. Although the function writer is ourselves, the function caller is not us, but other modules we reference, that is, third-party libraries. We call the functions in the third-party library and pass the callback function To the third-party library, the function in the third-party library calls the callback function we wrote, as shown in the figure:

The reason why it is necessary to specify a callback function for the third-party library is because the writer of the third-party library does not know what to do after certain specific nodes, such as the example we gave after the fried dough sticks are made, the network data is received, the file is read, etc. What to do is only known by the user of the library. Therefore, the writer of the third-party library cannot write code for the specific implementation, but can only provide a callback function to the outside world. The user of the library can implement the function. The third-party library can Just call the callback function on the node. Another point worth noting is that from the picture we can see that the callback function is in the same layer as our main program . We are only responsible for writing the callback function, but it is not called by us. The last thing worth noting is the time node when the callback function is called. The callback function is only called at certain specific nodes, such as the completion of making fried dough sticks, receiving network data, completing file reading, etc. These are all events. , that is, event. Essentially, the callback function we write is used to process events. Therefore, from this perspective, the callback function is nothing more than an event handler. Therefore, the callback function is naturally suitable for event-driven programming. We will discuss this in subsequent articles. Return to this theme again.

callback type

We already know that there are two types of callbacks. The difference between these two types of callbacks lies in the timing when the callback function is called. Note that the concepts of synchronization and asynchronousness will be used next.

Synchronous callbacks are commonly known as synchronous callbacks, and some call them blocking callbacks, or they have no modifications, just callbacks. This is the callback method we are most familiar with. When we call a function A and pass in the callback function as a parameter, the callback function will be executed before A returns. That is to say, our main program will wait for the callback function to complete execution. This is the so-called synchronous callback.

There are synchronous callbacks and asynchronous callbacks.

Asynchronous callbacks are different from synchronous callbacks. When we call a function A and pass in the callback function in the form of parameters, function A will return immediately. That is to say, function A will not block our main program and the callback function will be called after a period of time. Starts to be executed. At this time, our main program may be busy with other tasks. The execution of the callback function is performed at the same time as the running of our main program. Since the execution of our main program and callback function can occur at the same time, in general, the execution of the main program and callback function are located in different threads or processes.

This is the so-called asynchronous callbacks, and some information also calls it deferred callbacks. The name is very vivid, delayed callbacks. We can also see from the two pictures above that asynchronous callbacks can make full use of machine resources than synchronous callbacks. The reason is that in synchronous mode, the main program will be "lazy" and paused because calling other functions is blocked. However, Asynchronous calls do not have this problem, and the main program will continue to run. Therefore, asynchronous callbacks are more common in I/O operations and are naturally suitable for high-concurrency scenarios such as web services.

Programming thinking mode corresponding to callbacks

Let us use a few simple sentences to summarize the differences between callbacks and conventional programming thinking modes. Suppose we want to process a certain task, which depends on a certain service S. We can divide the task processing into two parts, the part PA before calling service S, and the part PB after calling service S. In normal mode, both PA and PB are executed by the service caller, that is, we execute the PA part ourselves and wait for the service S to return before executing the PB part. But it's different in the callback method. In this case, we execute the PA part ourselves, and then tell the service S: "Execute the PB part after you complete the service." So we can see that a task is now completed collaboratively by different modules. Right now:

  • Normal mode: After calling the S service, I will perform the X task.
  • Callback mode: After calling the S service, you then execute the X task.

Among them, X is set by the service caller, and the difference lies in who will execute it.

Why asynchronous callbacks are increasingly important

In synchronous mode, the service caller will be blocked and suspended due to service execution, which will cause the entire thread to be blocked. Therefore, this programming method is naturally not suitable for high-speed concurrent connection scenarios with tens of thousands or hundreds of thousands. In this concurrency scenario, asynchronous is actually more efficient. The reason is very simple. You don't need to wait in place, so you can make better use of machine resources, and the callback function is an indispensable mechanism under asynchronous conditions.

callback hell, callback hell

Some students may think that with an asynchronous callback mechanism that can handle all high-concurrency scenarios, they can sit back and relax. In fact, there is no technology in computer science that can sweep away and cure all diseases. There is not now and there will not be in the foreseeable future. Everything is the result of compromise. So what are the problems with the asynchronous callback mechanism? In fact, we have seen that the asynchronous callback mechanism is different from the synchronization mode that programmers are most familiar with. It is not as comprehensible as synchronization. If the business logic is relatively complex, for example, when we process a certain task, we need to call more than one services, but several or even dozens of services. If these service calls are all processed using asynchronous callbacks, then it is very likely that we will fall into callback hell. For example, suppose we need to call four services to process a certain task, and each service needs to rely on the results of the previous service. If it is implemented in a synchronous manner, it may look like this:

a = GetServiceA();
b = GetServiceB(a);
c = GetServiceC(b);
d = GetServiceD(c);

The code is clear and easy to understand. We know that asynchronous callbacks will be more efficient, so what will it be like to write using asynchronous callbacks?

GetServiceA(function(a){
    GetServiceB(a, function(b){
        GetServiceC(b, function(c){
            GetServiceD(c, function(d) {
                ....
            });
        });
    });
});

I think there is no need to emphasize anything further. Which of these two writing methods do you think is easier to understand and the code is easier to maintain? The blogger is fortunate enough to have maintained this type of code. I have to say that every time I add a new feature, I wish I could become two clones. One has to reread one side of the code; the other is scolding myself for why I chose to maintain this project in the first place. . Asynchronous callback code will fall into the callback trap if you are not careful. So is there a better way to combine the efficiency of asynchronous callback with the simplicity and readability of synchronous coding? Fortunately, the answer is yes, and we will explain this technology in detail in a subsequent article.

Summarize

In this article, we explain in detail the ins and outs of the callback function mechanism based on a practical example. This is an extremely important coding mechanism to deal with high concurrency and high performance scenarios. Asynchronous addition of callbacks can make full use of machine resources. , In fact, asynchronous callback is essentially event-driven programming, which is what we will focus on next.

Original author: Coder’s Desert Island Survival

 

Guess you like

Origin blog.csdn.net/youzhangjing_/article/details/132476147