Coroutines, also known as micro-threads, fibers. English name Coroutine.
The concept of coroutines has been around for a long time, but it was only in the last few years that it became widely used in some languages (such as Lua).
Subroutines, or functions, are called hierarchically in all languages. For example, A calls B, B calls C during execution, C returns after execution, B returns after execution, and finally A completes execution.
Therefore, the subroutine call is implemented through the stack, and a thread executes a subroutine.
Subroutine calls are always one entry, one return, and the calling sequence is unambiguous. Coroutine calls are different from subroutines.
A coroutine looks like a subroutine, but during execution, it can be interrupted inside the subroutine, and then turn to execute other subroutines, and then return to continue execution at an appropriate time.
Note that interrupting in a subroutine to execute other subroutines is not a function call, which is somewhat similar to CPU interrupts. For example, subroutines A and B:
def A(): print('1') print('2') print('3') def B(): print('x') print('y') print('z')
Assuming that it is executed by a coroutine, in the process of executing A, it can be interrupted at any time to execute B, and B may also be interrupted during the execution process and then execute A. The result may be:
1 2 x and 3 of
But B is not called in A, so the call of the coroutine is more difficult to understand than the function call.
It seems that the execution of A and B is a bit like multi-threading, but the feature of coroutine is that it is executed by one thread. Compared with multi-threading, what advantage does coroutine have?
The biggest advantage is the extremely high execution efficiency of the coroutine. Because subroutine switching is not thread switching, but is controlled by the program itself, there is no overhead of thread switching. Compared with multi-threading, the more threads there are, the more obvious the performance advantage of coroutines.
The second advantage is that there is no need for a multi-threaded locking mechanism, because there is only one thread, and there is no conflict of writing variables at the same time. In the coroutine, the shared resources are controlled without locking, and only the status needs to be judged, so the execution efficiency is higher. The thread is much higher.
Because the coroutine is executed by a thread, how to use the multi-core CPU? The simplest method is multi-process + coroutine, which not only makes full use of multi-core, but also gives full play to the high efficiency of coroutine, and can obtain extremely high performance.
Python's support for coroutines is implemented through generators.
In generators, we can not only iterate through for
loops, but also keep calling next()
functions to get yield
the next value returned by the statement.
But Python can yield
not only return a value, it can also receive parameters issued by the caller.
Let's see an example:
The traditional producer-consumer model is that a thread writes messages, a thread takes messages, and controls queues and waiting through a lock mechanism, but deadlocks may occur if you are not careful.
If the coroutine is used instead, after the producer produces the message, it directly yield
jumps to the consumer to start execution. After the consumer finishes executing, it switches back to the producer to continue production, which is extremely efficient:
def consumer(): r = '' while True: n = yield r if not n: return print('[CONSUMER] Consuming %s...' % n) r = '200 OK' def produce(c): c.send(None) n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % n) r = c.send(n) print('[PRODUCER] Consumer return: %s' % r) c.close() c = consumer() produce(c)
Results of the:
[PRODUCER] Producing 1... [CONSUMER] Consuming 1... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 2... [CONSUMER] Consuming 2... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 3... [CONSUMER] Consuming 3... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 4... [CONSUMER] Consuming 4... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 5... [CONSUMER] Consuming 5... [PRODUCER] Consumer return: 200 OK
Notice that the consumer
function is one generator
, after consumer
passing one produce
in:
-
First call
c.send(None)
the startup generator; -
Then, once something is produced, by
c.send(n)
switching toconsumer
execute; -
consumer
Byyield
getting the message, processing it, and passingyield
the result back; -
produce
Getconsumer
the result of the processing and continue to produce the next message; -
produce
Decided not to produce, byc.close()
closingconsumer
, the whole process ended.
The entire process is lock-free, executed by one thread, produce
and consumer
cooperates to complete tasks, so it is called "coroutine" rather than preemptive multitasking of threads.
Finally, a sentence of Donald Knuth summarizes the characteristics of coroutines: "Subroutines are a special case of coroutines."