GIL global interpreter locks, mutexes and coroutines

Table of Contents

1. Comparison of processes and threads

2. GIL global interpreter lock

2.1 Background information

2.2 Summary

3. Mutex lock

4. Thread Queue

4.1 Why are queues still used in threads?

4.2 First in, first out

4.3 Last in, first out

4.4 Priority queue

5. Use of process pool and thread pool

5.1 Basic methods

5.2 Use

5.3 Multi-threaded crawling of web pages

6. Coroutine

6.1 Coroutine basics

6.2 Coroutine gevent module

6.2.1 Functions of monkey patching (everything is an object)

6.2.2 Application scenarios of monkey patch

6.2.3 Example 1 (automatic switching when encountering io)

6.2.4 Example 2

6.3 Coroutines achieve high concurrency

Server

client


1. Comparison of processes and threads

1. The overhead of processes is much greater than that of threads

2. Data between processes is isolated, but data between threads is not isolated.

3. Thread data between multiple processes is not shared—–>Thread communication (IPC) under the process——->Queue

2. GIL global interpreter lock

The execution of Python code is controlled by the Python interpreter. When Python was designed, it was considered that in the main loop, only one thread would be executing at the same time. Although multiple threads can be “running” in the Python interpreter, only one thread is running in the interpreter at any time.

Access to the Python interpreter is controlled by the Global Interpreter Lock (GIL). It is this lock that ensures that there is only one thread at a time.

The process is running.

2.1 Background information

1. Python code runs on the interpreter and is executed or interpreted by the interpreter

2. Types of Python interpreters:
1. CPython 2, IPython 3, PyPy 4, Jython 5, IronPython

3. The most commonly used (95%) interpreter in the current market is the CPython interpreter

4. The GIL global interpreter lock exists in CPython

5. The conclusion is that only one thread is executing at the same time? The problem you want to avoid is that multiple threads compete for resources.

For example: Now start a thread to recycle garbage data and recycle the variable a=1. Another thread will also use this variable a. When the garbage collection thread has not finished recycling variable a, another thread will snatch this variable. a use.
How to avoid this problem is that in the design of the Python language, a lock is added directly to the interpreter. This lock is to allow only one thread to execute at the same time. The implication is which thread wants to execute. You must first get the lock (GIL). Only when this thread releases the GIL lock can other threads get it and then have execution permissions.

Conclusion: The GIL lock ensures that only one thread is executed at the same time. All threads must obtain the GIL lock to have execution permissions.

2.2 Summary

  1. The reason why Python has GIL lock is that multiple threads in the same process are actually executing at the same time.
  2. Only Python is used to open processes. Other languages generally do not open multiple processes. It is enough to open multiple threads.
  3. The CPython interpreter cannot take advantage of multi-core by running multiple threads. Only by running multiple processes can it take advantage of multi-core. This problem does not exist in other languages.
  4. 8-core CPU computer, make full use of my 8-core, at least 8 threads, all 8 threads are calculations—->The computer CPU usage is 100%,
  5. If there is no GIL lock, if 8 threads are started in one process, it can make full use of CPU resources and run to full capacity of the CPU.
  6. A lot of code and modules in the CPython interpreter are written based on the GIL lock mechanism and cannot be changed —> We cannot have 8 cores, but I can only use 1 core now, —-> enable multi-process- –> The threads started under each process can be scheduled and executed by multiple CPUs
  7. CPython interpreter: IO-intensive uses multi-threading, computing-intensive uses multi-process

IO intensive, the CPU will be switched when encountering IO operations. Suppose you open 8 threads, and all 8 threads have IO operations —–>IO operations do not consume CPU—->For a period of time, it seems, in fact All 8 threads have been executed. It is better to choose multi-threading.
It is computationally intensive and consumes CPU. If 8 threads are opened, the first thread will always occupy the CPU and will not be scheduled to other threads for execution. The other 7 threads are not executed at all, so we open 8 processes, each The process has one thread, and the threads under 8 processes will be executed by 8 CPUs, resulting in high efficiency.

Supplement: For computationally intensive applications, it is better to choose multi-process. In other languages, multi-threading is chosen instead of multi-process.

3. Mutex lock

In the case of multi-threading, if one data is executed at the same time, data confusion will occur.

n = 20
from threading import Lock
import time


def task(lock):
    lock.acquire()
    global n
    temp=n
    time.sleep(0.5)
    n=temp-1
    lock.release()


"""Exchange time for space and space for time. Time complexity"""
from threading import Thread

if __name__ == '__main__':
    tt = []
    lock = Lock()
    for i in range(10):
        t = Thread(target=task, args=(lock,))
        t.start()
        tt.append(t)
    for j in tt:
        j.join()

    print("main", n) # main 10

Replenish:

Interview question: Since we have a GIL lock, why do we need a mutex lock? (Under multi-threading)

For example: I started 2 threads to execute a=a + 1, a is 0 at the beginning

1. The first thread comes, gets a=0, and starts executing a=a + 1. At this time, the result a is 1.
2. The result 1 obtained by the first thread has not been assigned back to a. At this time, the second thread comes and gets a 0 and continues execution.
a=a + 1 the result is still 1
3. Adding a mutex lock can solve the problem of confusion when operating the same data under multiple threads.

Four. Thread Queue

4.1 Why are queues still used in threads?

Data of multiple threads in the same process is shared
Why does the same process still use queues in the first place?
Because the queue is a pipe + lock
Therefore, queues are used to ensure data security.

4.2 First in first out

'''
The disadvantage of queue.Queue is that its implementation involves multiple locks and condition variables.
Performance and memory efficiency may therefore be affected.
'''
import queue
queue.Queue()

q = queue.Queue() # infinite
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
Result (first in, first out):
first
second
third
'''

4.3 Last in, first out

import queue

'Lifo: last in first out'
q = queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
Result (last in, first out):
third
second
first
'''

4.4 Priority Queue

import queue

q = queue.PriorityQueue()
'put enters a tuple, the first element of the tuple is the priority (usually a number, it can also be a comparison between non-numbers),
The smaller the number, the higher the priority'
q.put((20, 'a'))
q.put((10, 'b'))
q.put((30, 'c'))

print(q.get())
print(q.get())
print(q.get())
'''
Result (the smaller the number, the higher the priority, and those with higher priority will be dequeued first):
(10, 'b')
(20, 'a')
(30, 'c')
'''

5. Use of process pool and thread pool

Pool: Pool, container type, can hold multiple elements

Process pool: Define a pool in advance, and then add processes to this pool. In the future, you only need to drop tasks into this process pool, and then any process in this process pool will execute the task.

Thread pool: Define a pool in advance, and then add threads to this pool. In the future, you only need to drop tasks into this thread pool, and then any thread in this thread pool will execute the task.

5.1 Basic Method

submit(fn, *args, **kwargs): Submit task asynchronously

map(func, *iterables, timeout=None, chunksize=1): replaces the for loop submit operation

shutdown(wait=True): equivalent to the pool.close() + pool.join() operation of the process pool

  • wait=True, wait for all tasks in the pool to be executed and resources to be recycled before continuing.
  • wait=False, returns immediately and does not wait for the tasks in the pool to be completed.
  • But regardless of the value of the wait parameter, the entire program will wait until all tasks are completed.
  • submit and map must precede shutdown

result(timeout=None): Get the result

add_done_callback(fn): callback function

done(): Determine whether a thread is completed

cancel(): Cancel a task

5.2 Use

def task(n, m):
    return n + m


def task1():
    return {'username': 'kevin', 'password': 123}


"""Open process pool"""
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor


def callback(res):
    print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
    print(res.result()) # 3


def callback1(res):
    print(res) # <Future at 0x2ad4ae50bb0 state=finished returned dict>
    print(res.result()) # {'username': 'kevin', 'password': 123}
    print(res.result().get('username')) # kevin


if __name__ == '__main__':
    pool = ThreadPoolExecutor(3) # Define a process pool with 3 processes in it
    # 2. Throw tasks into the pool

    pool.submit(task, m=1, n=2).add_done_callback(callback)
    pool.submit(task1).add_done_callback(callback1)
    pool.shutdown() # join + close
    print(123) # 123

5.3 Multi-threaded crawling of web pages

import requests

def get_page(url):
    res = requests.get(url)
    name = url.rsplit('/')[-1] + '.html'
    return {'name':name,'text':res.content}

def call_back(fut):
    print(fut.result()['name'])
    with open(fut.result()['name'],'wb') as f:
        f.write(fut.result()['text'])


if __name__ == '__main__':
    pool = ThreadPoolExecutor(2)
    urls = ['http://www.baidu.com','http://www.cnblogs.com',
'http://www.taobao.com']
    for url in urls:
        pool.submit(get_page,url).add_done_callback(call_back)

6. Coroutine

6.1 Basics of Coroutines

Previously, we learned the concepts of threads and processes, and understood that processes are the basic unit of resource allocation in the operating system, and threads are the smallest unit of CPU scheduling. Logically speaking, we have improved the CPU utilization a lot. But we know that whether we create multiple processes or create multiple threads to solve the problem, it will take a certain amount of time to create processes, create threads, and manage switching between them.

As our pursuit of efficiency continues to improve, implementing concurrency based on a single thread has become a new topic, that is, achieving concurrency with only one main thread (obviously there is only one available CPU). This saves the time spent creating thread processes.

To do this we need to first review the nature of concurrency: switching + saving state. The previous concurrent switching was actually switching processes or threads. The CPU is running a task and will switch to other tasks in two situations (switching is forcibly controlled by the operating system). One situation is that the task is blocked, and the other situation is that the task calculation time is too long.

Coroutine: It is concurrency under single thread, also known as micro-thread and fiber. The English name is Coroutine. One sentence explains what a coroutine is: a coroutine is a lightweight thread in user mode, that is, the coroutine is controlled and scheduled by ourselves and does not actually exist in the operating system.

Compared with the switching of threads controlled by the operating system, the user controls the switching of coroutines within a single thread.

The advantages are as follows:

  1. Coroutines are the most resource-saving, processes are the most resource-consuming, followed by threads
  2. Concurrency effects can be achieved within a single thread, maximizing utilization of the CPU.

The disadvantages are as follows:

  1. The essence of coroutine is that it is single-threaded and cannot use multiple cores. It can be that one program starts multiple processes, multiple threads are started in each process, and coroutines are started in each thread.
  2. Coroutine refers to a single thread, so once the coroutine blocks, the entire thread will be blocked.

6.2 The gevent module of the coroutine

6.2.1 The function of monkey patch (everything is an object)

It has the function of replacing when the module is running, for example: assigning a function object to another function object (replacing the original execution function of the function).

class Monkey():
    def play(self):
        print('Monkey is playing')

classDog():
    def play(self):
        print('The dog is playing')
m=Monkey()
m.play()
m.play = Dog().play
m.play()

6.2.2 Application scenarios of monkey patch

Here is a more practical example. Many people use import json, but later found that ujson has higher performance. If you think it is more expensive to change the import json of each file to import ujson as json, or you want to test whether the ujson replacement meets expectations. , just need to add at the entrance:

import json
import ujson

def monkey_patch_json():
    json.__name__ = 'ujson'
    json.dumps = ujson.dumps
    json.loads = ujson.loads
monkey_patch_json()
aa = json.dumps({'name':'lqz','age':19})
print(aa)

6.2.3 Example 1 (Automatically switch when io is encountered)

import gevent
import time


def eat(name):
    print('%s eat 1' % name)
    gevent.sleep(2)
    print('%s eat 2' % name)


def play(name):
    print('%s play 1' % name)
    gevent.sleep(1)
    print('%s play 2' % name)


# eat('kevin')
# play('jerry')
start_time = time.time()
g1 = gevent.spawn(eat, 'lqz')
g2 = gevent.spawn(play, name='lqz')
g1.join()
g2.join() # Equivalent to completing these tasks
# Or gevent.joinall([g1,g2])
print('main', time.time() - start_time)

The results are as follows:

'''
lqz eat 1
lqz play 1
lqz play 2
lqz eat 2
Main 2.0305111408233643
'''

6.2.4 Example 2

'''
The above example gevent.sleep(2) simulates io blocking that gevent can recognize.

However, time.sleep(2) or other blocking cannot be directly recognized by gevent. You need to use the following line of code and patch it to identify it.

from gevent import monkey;monkey.patch_all() must be placed in front of the person being patched, such as time, before the socket module

Or we can simply remember: to use gevent, you need to put from gevent import monkey;monkey.patch_all() at the beginning of the file
'''
from gevent import monkey; monkey.patch_all()

import gevent
import time
def eat():
    print('eat food 1')
    time.sleep(2)
    print('eat food 2')

def play():
    print('play 1')
    time.sleep(1)
    print('play 2')

start_time = time.time()
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)

g1.join()
g2.join()
#gevent.joinall([g1,g2])
print('main', time.time() - start_time)

The results are as follows:

'''
eat food 1
play 1
play 2
eat food 2
Main 2.0193047523498535
'''

6.3 Coroutine to achieve high concurrency

Server

from gevent import monkey;

monkey.patch_all()
import gevent
from socket import socket
# from multiprocessing import Process
from threading import Thread


def talk(conn):
while True:
try:
data = conn.recv(1024)
if len(data) == 0: break
print(data)
conn.send(data.upper())
except Exception as e:
print(e)
conn.close()


def server(ip, port):
server = socket()
server.bind((ip, port))
server.listen(5)
while True:
conn, addr = server.accept()
# t=Process(target=talk,args=(conn,))
# t=Thread(target=talk,args=(conn,))
#t.start()
gevent.spawn(talk, conn)


if __name__ == '__main__':
g1 = gevent.spawn(server, '127.0.0.1', 8080)
g1.join()

Client

import socket
from threading import current_thread, Thread


def socket_client():
    cli = socket.socket()
    cli.connect(('127.0.0.1', 8080))
    while True:
        ss = '%s say hello' % current_thread().getName()
        cli.send(ss.encode('utf-8'))
        data = cli.recv(1024)
        print(data)


for i in range(50000):
    t = Thread(target=socket_client)
    t.start()