(Link)
To find the solution to the second proposition, let's look back at the first server program in this article. As mentioned above, the problem with the first version of the program is that a thread serves a connection, while the overhead of the OS switching thread is very high, resulting in performance failure. But the first version of the program is definitely pleasant sequential programming. If we want to retain sequential programming, how should we overcome performance defects?
The problem is directly pointed to: Since the OS scheduling thread is very difficult, is there a "user-state thread" that is scheduled by the program itself to allow the OS to play?
The answer is thrown out first. The so-called "user-state thread" is generally implemented as "coroutine )".
In textbooks, the definition of coroutine is like this: "coroutine is a child routine of collaboration ". What? What does "collaboration" mean? What is "subroutine? This definition is truly confusing.
I think coroutine is difficult to define with a short statement. The concept of coroutine must be explained as follows:
1. coroutine is essentially an algorithm. If a function is interrupted and can continue to be executed as usual from the breakpoint during subsequent recovery, this function can be called a coroutine.
2. The essential difference between a coroutine and a thread is that the threads compete for the CPU, while the coroutine cooperates with each other and the CPU is "managed" by each other. The coroutine has a key primitive "yield", indicating that the CPU is automatically withdrawn. Yield is called by the user in the coroutine. Theoretically, you can choose not to call yield, so this coroutine exclusively occupies the CPU resources of the entire thread. Coroutine and yield are the intention of coroutine collaboration.
Many programming languages naturally support coroutines at the language level, such as C #, go, and Lua. Unfortunately, the underlying C/C ++ is not supported in languages.
Of course, C/C ++ can implement coroutine by itself. Windows has a coroutine API (fiber). Linux provides the ucontext. h header file that allows users to save and reply to the breakpoint context (current register status and stack memory) to implement coroutine. If you want to learn more details, read the coroutine code I implemented: http://github.com/xphh/coroutine
With coroutine, there is still a distance from our proposition "sequential programming. It's only half done.
Because the coroutine is still in a thread after all, a coroutine is blocked and other coroutine cannot run. That is to say, coroutine cannot be equivalent to a "user-state thread ". To use coroutine as a thread, you must consider how to change the blocked operations in the coroutine into non-blocking operations in the thread.
"Turning blocked operations in the coroutine into non-blocking operations in the thread" is difficult to explain directly. I can only explain how to do this.
In the first version of the server program, when we process a connection, we often need to block the Recv function when receiving it. But in fact, the Recv process is nothing more than waiting for Io data. During the waiting process, the CPU is wasted. So the first thing we need to do is to get yield to an epoll coroutine before Recv blocking. This epoll coroutine listens to all Io. When an IO (for example, the IO in the coroutine just now) has data, resume returns to the coroutine where the IO is located.
We mentioned the resume primitive of the coroutine above. In this case, coroutine has two programming models: one is called Symmetric coroutine, where all coroutine can be any yield to another coroutine, and the other is called asymmetric coroutine ", all coroutines can only be yield to the main coroutine, and the main coroutine uses resume to call a coroutine. In general, we recommend asymmetric coroutine because symmetric coroutine is messy in programming.
In this way, the entire program is composed of N + 1 coroutine of a thread. N coroutines process n connections, and one master coroutine performs Io multiplexing (epoll ). Our processing on these N coroutines is indeed blocking, but in fact the thread is not blocking, and most of the time the thread is on the epoll of the main coroutine.
That is to say, to use coroutine sequence programming, We must process all blocking calls in advance. All socket interfaces (connect, sendto, send, recvfrom, Recv) and sleep functions need to be implemented as the coroutine version, that is, yield goes out Before blocking, in the main coroutine, wait for the event to trigger and then resume to return.
Now, Proposition 2 is implemented!
Is it over? No. If you want to use coroutine for Server programming, you must know the following:
1. coroutine is only a thread with independent stack space. If you are blocked in the coroutine, other coroutine is also blocked.
2. I believe everyone knows the last point. Are you sure you want to say that you can rewrite the blocking function? Yes. However, you must understand that C/C ++ programming is more of an ecosystem. Unfortunately, this ecosystem is not friendly to the coroutine process. We almost use servers to develop related third-party libraries, such as mysqlclient and libcurl. If it is used in projects, it is impossible to directly change the source code. Therefore, you must consider the boundaries used by the coroutine.
3. For multi-core servers, multi-thread or multi-process is required to improve performance. If you want to use coroutine in multiple threads, remember not to schedule coroutine in multiple threads. This is not to say no, but the starting point of this approach and the process is different.
Multithreading-> event model-> coroutine, this is the path to Server programming.
The path to Server programming: endless (lower)