【Python】Coroutine

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 forloops, but also keep calling next()functions to get yieldthe next value returned by the statement.

But Python can yieldnot 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 yieldjumps 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 consumerfunction is one generator, after consumerpassing one producein:

  1. First call c.send(None)the startup generator;

  2. Then, once something is produced, by c.send(n)switching to consumerexecute;

  3. consumerBy yieldgetting the message, processing it, and passing yieldthe result back;

  4. produceGet consumerthe result of the processing and continue to produce the next message;

  5. produceDecided not to produce, by c.close()closing consumer, the whole process ended.

The entire process is lock-free, executed by one thread, produceand consumercooperates 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."

 

Guess you like

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